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
- Hermes with LM Studio backend on a Qwen3 thinking model (e.g.
qwen3.6-35b-a3b-uncensored), chat_template_kwargs.enable_thinking: true.
- Enable
observability/langfuse + set HERMES_LANGFUSE_* env vars.
- Run any turn that triggers reasoning.
- 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.reasoning ← msg.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.
Bug Description
The bundled
observability/langfuseplugin's_serialize_assistant_messagereads only the top-levelreasoningattribute:Providers that emit chain-of-thought via
reasoning_content(LM Studio, Moonshot, Qwen3 thinking models, DeepSeek) get silently dropped. The transport correctly stores them onNormalizedResponse.reasoning_content(a property reading fromprovider_data["reasoning_content"], seetransports/types.py:115), but the plugin never reads that field. EveryLLM call Nobservation showsreasoning: Noneeven though Hermes captures the reasoning fine (visible in CLI/Telegram vialast_reasoninginrun_agent.py).Independent of #26320, which fixed the missing
assistant_messagekwarg on the hook payload. The serialization gap is still in upstream HEAD.Steps to Reproduce
qwen3.6-35b-a3b-uncensored),chat_template_kwargs.enable_thinking: true.observability/langfuse+ setHERMES_LANGFUSE_*env vars.LLM call Ngeneration in Langfuse.Direct LM Studio response confirming
reasoning_contentis the field actually used:Expected Behavior
output.reasoningon each generation observation contains the chain-of-thought. The same text is already extracted by_extract_reasoning(run_agent.py) andextract_content_or_reasoning(agent/auxiliary_client.py:4404) for other consumers; Langfuse should get it too.Actual Behavior
output.reasoningisNoneon every generation observation. Live tracec2acb87a9ac6...from my deployment, generationLLM call 12: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:NormalizedResponse.reasoning←msg.reasoning(OpenAI convention)provider_data["reasoning_content"]←msg.reasoning_content(DeepSeek convention)NormalizedResponse.reasoning_contentexposes the second via a property (transports/types.py:115). The Langfuse plugin's serializer only reads the first.Proposed Fix
Extend
_serialize_assistant_messageto walkreasoning,reasoning_content,reasoning_details(deduplicating). Same precedence asextract_content_or_reasoninginagent/auxiliary_client.py. No inline<think>regex fallback in the plugin; that's owned by_build_assistant_message.I can put up a PR.