hermes - 💡(How to fix) Fix [Bug]: Langfuse plugin shows reasoning: None for reasoning_content models (DeepSeek/Qwen/LM Studio convention)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Root Cause

In chat_completions.py:563-583, the transport splits reasoning across two destinations:

  • top-level NormalizedResponse.reasoningmsg.reasoning (OpenAI convention)
  • provider_data["reasoning_content"]msg.reasoning_content (DeepSeek convention)

NormalizedResponse.reasoning_content exposes the second via a property (transports/types.py:115). The Langfuse plugin's serializer only reads the first.

Fix Action

Fix / Workaround

content: '[573 chars]'  # placeholder; fixed by #26320 in HEAD
reasoning: None

Code Example

# plugins/observability/langfuse/__init__.py (HEAD: 31a010010)
def _serialize_assistant_message(message: Any) -> dict[str, Any]:
    return {
        "content": _safe_value(getattr(message, "content", None)),
        "reasoning": _safe_value(getattr(message, "reasoning", None)),
        "tool_calls": _serialize_tool_calls(getattr(message, "tool_calls", None)),
    }

---

Message keys: ['content', 'reasoning_content', 'role', 'tool_calls']

[content] (110 chars): \n\nThinking: Adding two to two ... Answer: 4
[reasoning_content] (796 chars): Here's a thinking process:\n\n1.  **Analyze User Input:** ...

---

content: '[573 chars]'  # placeholder; fixed by #26320 in HEAD
reasoning: None
RAW_BUFFERClick to expand / collapse

Bug Description

The bundled observability/langfuse plugin's _serialize_assistant_message reads only the top-level reasoning attribute:

# plugins/observability/langfuse/__init__.py (HEAD: 31a010010)
def _serialize_assistant_message(message: Any) -> dict[str, Any]:
    return {
        "content": _safe_value(getattr(message, "content", None)),
        "reasoning": _safe_value(getattr(message, "reasoning", None)),
        "tool_calls": _serialize_tool_calls(getattr(message, "tool_calls", None)),
    }

Providers that emit chain-of-thought via reasoning_content (LM Studio, Moonshot, Qwen3 thinking models, DeepSeek) get silently dropped. The transport correctly stores them on NormalizedResponse.reasoning_content (a property reading from provider_data["reasoning_content"], see transports/types.py:115), but the plugin never reads that field. Every LLM call N observation shows reasoning: None even though Hermes captures the reasoning fine (visible in CLI/Telegram via last_reasoning in run_agent.py).

Independent of #26320, which fixed the missing assistant_message kwarg on the hook payload. The serialization gap is still in upstream HEAD.

Steps to Reproduce

  1. Hermes with LM Studio backend on a Qwen3 thinking model (e.g. qwen3.6-35b-a3b-uncensored), chat_template_kwargs.enable_thinking: true.
  2. Enable observability/langfuse + set HERMES_LANGFUSE_* env vars.
  3. Run any turn that triggers reasoning.
  4. Inspect the LLM call N generation in Langfuse.

Direct LM Studio response confirming reasoning_content is the field actually used:

Message keys: ['content', 'reasoning_content', 'role', 'tool_calls']

[content] (110 chars): \n\nThinking: Adding two to two ... Answer: 4
[reasoning_content] (796 chars): Here's a thinking process:\n\n1.  **Analyze User Input:** ...

Expected Behavior

output.reasoning on each generation observation contains the chain-of-thought. The same text is already extracted by _extract_reasoning (run_agent.py) and extract_content_or_reasoning (agent/auxiliary_client.py:4404) for other consumers; Langfuse should get it too.

Actual Behavior

output.reasoning is None on every generation observation. Live trace c2acb87a9ac6... from my deployment, generation LLM call 12:

content: '[573 chars]'  # placeholder; fixed by #26320 in HEAD
reasoning: None

Affected Component

Other (plugin: observability/langfuse)

OS / Python / Hermes Version

macOS 26.4.1, Python 3.11.15, Hermes v0.13.0. Verified bug is still present on main @ 31a010010.

Root Cause Analysis

In chat_completions.py:563-583, the transport splits reasoning across two destinations:

  • top-level NormalizedResponse.reasoningmsg.reasoning (OpenAI convention)
  • provider_data["reasoning_content"]msg.reasoning_content (DeepSeek convention)

NormalizedResponse.reasoning_content exposes the second via a property (transports/types.py:115). The Langfuse plugin's serializer only reads the first.

Proposed Fix

Extend _serialize_assistant_message to walk reasoning, reasoning_content, reasoning_details (deduplicating). Same precedence as extract_content_or_reasoning in agent/auxiliary_client.py. No inline <think> regex fallback in the plugin; that's owned by _build_assistant_message.

I can put up a PR.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING