hermes - ✅(Solved) Fix [Bug]: persisted assistant messages store reasoning in 'reasoning' (internal) instead of 'reasoning_content', leaving sessions silently poisoned for any future DeepSeek/Kimi thinking-mode replay [5 pull requests, 1 comments, 2 participants]

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…
GitHub stats
NousResearch/hermes-agent#16844Fetched 2026-04-29 06:38:33
View on GitHub
Comments
1
Participants
2
Timeline
16
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×6labeled ×5referenced ×2commented ×1

run_agent.py writes assistant turns to disk with the chain-of-thought stored under the internal field name reasoning, not the protocol-standard reasoning_content. The standard field is only persisted when the upstream SDK object happens to expose assistant_message.reasoning_content, which is provider-dependent. For most non-DeepSeek providers (GLM, MiniMax, GPT‑5.x via aigw / OpenAI Chat Completions wrappers) the field never gets written.

This means every assistant tool-call turn produced by those providers is silently poisoned at write time. The poison is invisible until the user later switches to a DeepSeek‑v4 / Kimi thinking model — which strictly requires reasoning_content on every replayed assistant turn — at which point HTTP 400 fires:

The reasoning_content in the thinking mode must be passed back to the API.

The recently merged read-side guards (#15213, #15741, #15748, #15353) all attempt to compensate at request-build time. They each fix one build path. But the underlying schema mismatch on disk means every new build path is a candidate for the same 400, and any session created by another provider becomes a latent bomb the moment the user switches model.

This issue is about the write side, not the read side. The proposal is to normalize the field name at persistence time so the read-side compensation code is unnecessary.

Error Message

"reasoning": "Let me analyze the health check output:\n\n- CRIT: 0\n- WARN: 1 - gateway_state hasn't been updated for over 27 hours (pid=75659)\n\nI need to investigate this warning about the gateway process. Let me che…",

Root Cause

Root cause in code

Fix Action

Fix / Workaround

Those issues all describe a single read path that fails to copy or inject reasoning_content when building the next API request. Each fix patches one path:

Workaround for affected users

PR fix notes

PR #16884: fix(agent): always persist reasoning_content on assistant turns

Description (problem / solution / changelog)

What

run_agent.py persists assistant turns with the chain of thought under the internal field reasoning and only writes the protocol-standard reasoning_content when:

  • the upstream SDK exposes assistant_message.reasoning_content as a top-level attribute, or
  • the current provider is DeepSeek and the turn has tool_calls (existing narrow guard).

Streaming-only providers (glm, MiniMax, gpt-5.x via aigw, Anthropic via openai-compat shims, etc.) emit reasoning through delta.reasoning_content chunks that get accumulated into the local reasoning_text string — but never land on the assistant message object as an attribute, so the persisted message is missing reasoning_content.

The bug is silent until the user later replays that history through a DeepSeek‑v4 / Kimi thinking model, which strictly requires reasoning_content on every replayed assistant turn:

The reasoning_content in the thinking mode must be passed back to the API.

Read-side patches (#15213, #15741, #15748, #15353) each fix one build path, but every new path that reads history from disk is a fresh place the same 400 can resurface.

Fix

Normalize at write time. At the point where the assistant message dict is built (around run_agent.py:8085), always populate reasoning_content:

  1. prefer the SDK-supplied assistant_message.reasoning_content when present (may carry structured data);
  2. otherwise fall back to the already-sanitized reasoning_text that was accumulated from streaming deltas;
  3. finally default to "" so non-thinking providers ignore it harmlessly while DeepSeek/Kimi see a valid (empty) value.

The internal reasoning alias is preserved for backward compatibility with existing read paths and downstream consumers — this PR is purely additive on the wire format.

This makes the four landed read-side fixes redundant safety nets rather than mandatory promotion paths.

Diff

run_agent.py | 27 +++++++++++++++++----------
1 file changed, 17 insertions(+), 10 deletions(-)

The change is local to the assistant-message persistence block; no behavioural change for currently-working DeepSeek paths (those already get reasoning_content from the SDK or from the existing tool-call guard).

Not in scope

  • Migration of existing poisoned session files under ~/.hermes/sessions/** — happy to follow up in a separate PR if maintainers want it; the original issue author offered one as well.
  • Removing the read-side _copy_reasoning_content_for_api promotion logic — leaving it in place as defense in depth.

Refs #16844

Changed files

  • run_agent.py (modified, +17/-10)

PR #16890: fix(opencode): re-derive api_mode per target model on /model switch

Description (problem / solution / changelog)

Salvages PR #16888 (opencode fix only — the second stacked reasoning_content commit from that PR is dropped; it is a separate concern tracked under #16844 and will be evaluated on its own).

Summary

/model deepseek-v4-flash from a minimax-m2.7 default on opencode-go / opencode-zen 404'd. The api_mode resolver honoured the persisted api_mode: anthropic_messages (set when minimax was default) before the opencode model registry, so the deepseek turn inherited anthropic_messages, stripped /v1 from base_url, and went to https://opencode.ai/zen/go/messages instead of .../v1/chat/completions.

Fixes #16878.

Changes

  • hermes_cli/runtime_provider.py — promote the opencode-zen/go branch above the configured_mode check in both api_mode resolution paths (_resolve_runtime_from_pool_entry and the API-key fallback in resolve_runtime_provider). opencode always re-derives mode from the effective target_model; other providers keep existing precedence (persisted api_mode > URL detection).
  • tests/hermes_cli/test_runtime_provider_resolution.py — rename + invert test_opencode_go_configured_api_mode_still_overrides_default (added in #4508) to lock in the new precedence. A persisted chat_completions on a minimax model no longer wins — the model dictates the mode. Escape-hatch for opencode is intentionally removed; the model name is the unambiguous signal.

Validation

BeforeAfter
minimax default → /model deepseek-v4-flashapi_mode=anthropic_messages, base_url=.../zen/go (404)api_mode=chat_completions, base_url=.../zen/go/v1
deepseek default → /model minimax-m2.7api_mode=anthropic_messages, base_url=.../zen/gounchanged
no target_model (legacy callers)unchangedunchanged

Suites: tests/hermes_cli/test_runtime_provider_resolution.py, test_model_switch_opencode_anthropic.py, test_model_switch_custom_providers.py — 109/109 pass. E2E verified against origin/main with a real config.yaml and isolated HERMES_HOME.

Credit: @Sanjays2402 for the original fix (#16888).

Changed files

  • hermes_cli/runtime_provider.py (modified, +19/-9)
  • tests/hermes_cli/test_runtime_provider_resolution.py (modified, +13/-2)

PR #16892: fix(agent): persist streamed reasoning_content on assistant turns (#16844)

Description (problem / solution / changelog)

Supersedes #16884 with a scoped rework that preserves existing read-side compensation.

Summary

Streaming-only providers (glm, MiniMax, gpt-5.x via aigw, Anthropic via openai-compat shims) accumulate reasoning through delta.reasoning_content chunks but never expose it as a top-level attribute on the finalized SDK message. The existing hasattr-guarded block at _build_assistant_message therefore never wrote reasoning_content for those providers, so the chain-of-thought was persisted only under the internal reasoning key.

The poison is silent until the user later switches to a DeepSeek-v4 or Kimi thinking model, at which point replay 400s with "The reasoning_content in the thinking mode must be passed back to the API." Issue #16844 reports 4,031 poisoned messages across 1,101 session files on one install.

Changes

  • run_agent.py _build_assistant_message: additive fallback promotes the already-sanitized reasoning_text to reasoning_content when no earlier branch wrote it and reasoning text was actually captured. Existing SDK-attr branch and DeepSeek ""-pad (#15250) are untouched.
  • tests/run_agent/test_run_agent.py: 3 regression tests — streaming promotion path, SDK precedence, field-absent-when-no-reasoning invariant.

Why not #16884 as-written

#16884 replaced the conditional with msg["reasoning_content"] = "" as a universal fallback, which would have:

  1. Triggered the Anthropic adapter's isinstance(reasoning_content, str) branch to prepend an empty {"type":"thinking","thinking":""} block on every replayed assistant turn.
  2. Sent reasoning_content: "" to every strict OpenAI-compatible provider (Mistral, Fireworks, stock OpenAI, GitHub Models).
  3. Short-circuited _copy_reasoning_content_for_api's step-1 string check on every turn, making tiers 2–4 dead code — including #15748's cross-provider reasoning-leak guard for DeepSeek/Kimi.

The layered approach here solves the write-side bug without changing the field's presence semantics for non-thinking sessions. Existing read-side ladder (cross-provider leak guard #15748, promote-from-reasoning, DeepSeek/Kimi thinking-pad) stays live as defense in depth.

Validation

BeforeAfter
Streaming reasoning persisted (glm, MiniMax, gpt-5.x via aigw)missing reasoning_content → 400 on DeepSeek/Kimi replaypromoted at write time
SDK-exposed reasoning_contentpreservedpreserved
DeepSeek tool-call ""-pad (#15250)firesfires
Non-thinking turns (no reasoning)field absentfield absent
_copy_reasoning_content_for_api tiers 2–4livelive
#15748 cross-provider leak guardlivelive
Targeted tests: TestBuildAssistantMessage + test_deepseek_reasoning_content_echon/a15 + 23 passing

Credit @Sanjays2402 for the original diagnosis in #16884.

Refs #16844, #16884, #15250, #15353, #15748.

Changed files

  • run_agent.py (modified, +25/-0)
  • tests/run_agent/test_run_agent.py (modified, +56/-0)

PR #16888: fix(opencode): re-derive api_mode per target model on /model switch

Description (problem / solution / changelog)

What

opencode-zen and opencode-go each serve both anthropic_messages models (e.g. minimax-m2.7) and chat_completions models (e.g. deepseek-v4-flash) behind a single base_url (https://opencode.ai/zen/go/v1). When the user starts a session with a minimax-m2.7 default and runs /model deepseek-v4-flash, the api_mode resolver in hermes_cli/runtime_provider.py honours the persisted model_cfg.api_mode (= anthropic_messages, set by the previous default) before checking the opencode model registry, so the deepseek turn inherits anthropic_messages, strips /v1 from base_url (the Anthropic SDK adds its own /v1/messages), and 404s.

Reported and root-caused in #16878.

Fix

Promote the opencode detection branch above the configured_mode check in both api_mode resolution paths:

  • _resolve_runtime_from_pool_entry — pool-backed providers (~line 262)
  • _resolve_api_key_runtime — API-key fallback (~line 1213)

Both branches now call opencode_model_api_mode(provider, effective_model) unconditionally for opencode-zen / opencode-go before considering any persisted api_mode, so the mode always reflects the model the user just switched to. Other providers retain the existing precedence (persisted api_mode > URL detection).

Tests

Existing suite passes:

$ pytest tests/hermes_cli/test_model_switch_opencode_anthropic.py
............                                                             [100%]
12 passed, 13 warnings in 4.31s

This covers both the minimax-m2.7 → deepseek-v4-flash direction (the failing case in the issue) and the reverse deepseek-v4-flash → minimax-m2.7, which stays correct because the same code path also re-derives the mode from the target model.

Diff

hermes_cli/runtime_provider.py | 28 +++++++++++++++++---------
1 file changed, 19 insertions(+), 9 deletions(-)

Fixes #16878

Changed files

  • hermes_cli/runtime_provider.py (modified, +19/-9)
  • run_agent.py (modified, +17/-10)

PR #17018: fix: resolve 7 identified issues [automated]

Description (problem / solution / changelog)

Resumo

Este PR corrige 7 issues identificados no repositório Hermes Agent, abrangendo bugs, endurecimento de segurança e compatibilidade cross-platform.

Issues Corrigidos

1. #16844 - Persisted assistant messages store reasoning incorrectly for DeepSeek/Kimi replay

  • Arquivo:
  • Problema: Mensagens assistentes persistidas armazenavam reasoning no campo interno 'reasoning' em vez de 'reasoning_content', envenenando sessões futuras com DeepSeek/Kimi thinking-mode replay.
  • Correção: Adicionado campo ao persistir mensagens de assistant.

2. #16809 - read_file has no sensitive-path deny list (Security - P1)

  • Arquivo:
  • Problema: A ferramenta read_file não tinha lista de negação para caminhos sensíveis. SSH keys, .env, e shell history eram livremente legíveis.
  • Correção: Adicionadas , , e integrada à para bloquear acesso a arquivos sensíveis.

3. #16730 / #16723 - Ollama provider API key e terminal timeout

  • Arquivo:
  • Problema: Provider ollama exigia API key desnecessariamente; timeout padrão do terminal era 60s quando o terminal_tool usa 180s.
  • Correção: Allow ollama provider sem API key; corrige fallback do terminal_timeout para 180s.

4. #16725 / #16713 - Discord known-thread bypass e rate-limit timeout

  • Arquivo:
  • Problema: Mensagens em threads conhecidas pelo bot ignoravam mention gating incondicionalmente; _safe_sync_slash_commands podia estourar timeout de 30s após mass prune.
  • Correção: Adicionado opt-in; separado sync de comandos críticos (fast_only=True) de cleanup de orphans via background task com rate-limit pacing.

5. #16720 - MEDIA: tags extraídas de qualquer tool output

  • Arquivo:
  • Problema: Regex MEDIA: era aplicado a todos os tool outputs, fazendo attachments spuriously de documentation/search/skills tools.
  • Correção: Allowlist de tool names () para extração de MEDIA:; mapeamento tool_call_id→tool_name.

6. #16703 - execute_code Docker DooD permission denied

  • Arquivo:
  • Problema: Erro genérico quando hermes user sem acesso ao docker.sock tentava usar Docker backend dentro de container (DoD).
  • Correção: Handler específico para PermissionError com mensagem explicativa sobre --group-add/docker GID.

Arquivos Modificados

ArquivoCommits
#16844
#16809
#16730, #16723
#16725, #16713
#16720
#16703

Como Testar

  1. Verificar que retorna erro de acesso negado
  2. Verificar que em config.yaml respeita mention gating em threads conhecidas
  3. Verificar que Docker backend no DooD mostra mensagem de erro informativa sobre --group-add
  4. Verificar que mensagens assistentes com reasoning são corretamente persistidas com reasoning_content

Gerado automaticamente em 2026-04-28T13:29:15Z

Changed files

  • agent/display.py (modified, +3/-1)
  • agent/file_safety.py (modified, +83/-1)
  • cli.py (modified, +6/-2)
  • gateway/platforms/api_server.py (modified, +10/-1)
  • gateway/platforms/dingtalk.py (modified, +22/-0)
  • gateway/platforms/discord.py (modified, +165/-6)
  • gateway/platforms/qqbot/adapter.py (modified, +12/-7)
  • gateway/run.py (modified, +22/-2)
  • hermes_cli/tools_config.py (modified, +1/-1)
  • run_agent.py (modified, +2/-1)
  • setup-hermes.sh (modified, +3/-2)
  • tools/environments/docker.py (modified, +76/-4)
  • tools/image_generation_tool.py (modified, +151/-0)
  • tools/mcp_tool.py (modified, +39/-4)
  • toolsets.py (modified, +2/-2)

Code Example

Scanned files       : 1 497
Files with poison   : 1 101   (assistant + tool_calls + missing reasoning_content)
Poisoned msgs total : 4 031

---

{
  "role": "assistant",
  "content": "",
  "reasoning": "Let me analyze the health check output:\n\n- CRIT: 0\n- WARN: 1 - gateway_state hasn't been updated for over 27 hours (pid=75659)\n\nI need to investigate this warning about the gateway process. Let me che…",
  "finish_reason": "tool_calls",
  "tool_calls": [2 calls … ]
}

---

msg = {
    ...
    "reasoning": reasoning_text,        # internal canonical name — always written
    "finish_reason": finish_reason,
}
if hasattr(assistant_message, "reasoning_content"):
    raw = getattr(assistant_message, "reasoning_content", None)
    if raw is not None:
        msg["reasoning_content"] = _sanitize_surrogates(raw)   # only when SDK exposed it
    elif msg.get("tool_calls") and self._needs_deepseek_tool_reasoning():
        msg["reasoning_content"] = ""                           # narrow guard, only when current provider is DeepSeek at write time

---

msg["reasoning_content"] = _sanitize_surrogates(reasoning_text or "")
RAW_BUFFERClick to expand / collapse

[Bug]: persisted assistant messages store reasoning in reasoning (internal) instead of reasoning_content, leaving sessions silently poisoned for any future DeepSeek/Kimi thinking-mode replay

Summary

run_agent.py writes assistant turns to disk with the chain-of-thought stored under the internal field name reasoning, not the protocol-standard reasoning_content. The standard field is only persisted when the upstream SDK object happens to expose assistant_message.reasoning_content, which is provider-dependent. For most non-DeepSeek providers (GLM, MiniMax, GPT‑5.x via aigw / OpenAI Chat Completions wrappers) the field never gets written.

This means every assistant tool-call turn produced by those providers is silently poisoned at write time. The poison is invisible until the user later switches to a DeepSeek‑v4 / Kimi thinking model — which strictly requires reasoning_content on every replayed assistant turn — at which point HTTP 400 fires:

The reasoning_content in the thinking mode must be passed back to the API.

The recently merged read-side guards (#15213, #15741, #15748, #15353) all attempt to compensate at request-build time. They each fix one build path. But the underlying schema mismatch on disk means every new build path is a candidate for the same 400, and any session created by another provider becomes a latent bomb the moment the user switches model.

This issue is about the write side, not the read side. The proposal is to normalize the field name at persistence time so the read-side compensation code is unnecessary.

Why this is distinct from #15213 / #15741 / #15748 / #15353

Those issues all describe a single read path that fails to copy or inject reasoning_content when building the next API request. Each fix patches one path:

  • #15213 — main loop after auxiliary calls
  • #15741 — cron path primary tool-result handoff
  • #15748 — _copy_reasoning_content_for_api ordering bug (cross-provider leak)
  • #15353 / #15250 / #15717 — earlier surface symptoms

This issue identifies the upstream cause: assistant messages are persisted with the wrong field name, so every read path has to reinvent a "promote reasoningreasoning_content (or inject "")" dance. Any code path that omits the dance — present or future — will fail.

The cumulative evidence below shows this is not theoretical: a single user's session store accumulated 4 031 poisoned messages across 1 101 session files, every one of which would 400 on DeepSeek replay despite all four landed fixes being present in tree.

Forensic data from a real install

Hermes Agent v0.11.0 (2026.4.23). After encountering the 400 with provider=custom, model=deepseek-v4-pro against https://aigw.netease.com/v1, I scanned the full session store at ~/.hermes/sessions/ and ~/.hermes/profiles/*/sessions/:

Scanned files       : 1 497
Files with poison   : 1 101   (assistant + tool_calls + missing reasoning_content)
Poisoned msgs total : 4 031

Breakdown of the 4 031 poisoned messages:

By session.model (top entries):

countmodel
3 651glm-5.1
272MiniMax-M2.7
74gpt-5.4
21MiniMax-M2.7-highspeed
11claude-opus-4-6
2deepseek-v4-pro

By message structure:

signalvaluemeaning
has internal reasoning field, non-empty string3 603 / 4 031 (89%)hermes captured the chain of thought, just under the wrong key
no reasoning at all428 / 4 031 (11%)message stored without any reasoning info
finish_reason == "tool_calls"3 501 / 4 031 (87%)classic tool-call termination
empty content3 027 / 4 031 (75%)pure function-call turns

Sample poisoned message (from a cron job that ran 2026-04-26 under glm-5.1):

{
  "role": "assistant",
  "content": "",
  "reasoning": "Let me analyze the health check output:\n\n- CRIT: 0\n- WARN: 1 - gateway_state hasn't been updated for over 27 hours (pid=75659)\n\nI need to investigate this warning about the gateway process. Let me che…",
  "finish_reason": "tool_calls",
  "tool_calls": [2 calls … ]
}

Note: the chain of thought was captured (267 chars under reasoning). It just isn't written under the name DeepSeek requires.

Root cause in code

run_agent.py (around line 7755):

msg = {
    ...
    "reasoning": reasoning_text,        # internal canonical name — always written
    "finish_reason": finish_reason,
}
if hasattr(assistant_message, "reasoning_content"):
    raw = getattr(assistant_message, "reasoning_content", None)
    if raw is not None:
        msg["reasoning_content"] = _sanitize_surrogates(raw)   # only when SDK exposed it
    elif msg.get("tool_calls") and self._needs_deepseek_tool_reasoning():
        msg["reasoning_content"] = ""                           # narrow guard, only when current provider is DeepSeek at write time

Two failure modes:

  1. The non-DeepSeek SDK object often doesn't expose reasoning_content as a top-level attribute (the data lives under delta.reasoning_content in streaming chunks, accumulated into the local variable reasoning_text, and then written only to the internal "reasoning" key). The standard field never lands on disk.
  2. The _needs_deepseek_tool_reasoning() guard only fires when the current provider is DeepSeek. If the message is being written under glm/minimax/gpt and the user later switches to DeepSeek, the guard never ran when it would have helped.

The read-side _copy_reasoning_content_for_api does have a path that promotes reasoningreasoning_content, and after #15748's reordering it does the right thing on the main loop. But every new code path that builds an API request from history (cron, fallback switch, auxiliary clients, ACP adapter, gateway replay, transports/chat_completions, transports/bedrock) is a fresh place where the dance can be forgotten — and #15213 / #15741 are evidence that this happens.

Reproduction

  1. Hermes v0.11.0, any non-DeepSeek thinking model that emits reasoning via delta.reasoning_content in streaming (e.g. glm-5.1 over an aigw or zhipu endpoint).
  2. Have at least one tool-call turn in the conversation.
  3. Inspect the persisted session JSON — the assistant turn will have "reasoning": "…" but no "reasoning_content" key.
  4. Switch the same session (or a new run that loads accumulated context, e.g. cron with persistent session, or an a2a sub-agent) to deepseek-v4-pro / deepseek-v4-flash.
  5. The next API request that replays the message returns HTTP 400.

In my install this happened at message ~100 of a session that had been growing for a day under glm-5.1, the moment the fallback chain promoted DeepSeek to primary.

Suggested fix

Normalize at write time, not at read time.

In the persistence path that builds the assistant message dict, write the chain of thought to reasoning_content directly (which is the standard cross-provider name; the SDK ecosystem has effectively converged on this), and either drop the reasoning alias or keep both for one release for backward compat.

Concretely: at the point where reasoning_text is finalized for the message, write:

msg["reasoning_content"] = _sanitize_surrogates(reasoning_text or "")

unconditionally for assistant turns. The empty string is the safest default — DeepSeek/Kimi accept it, every other provider ignores unknown empty fields, and the read side no longer needs to compensate.

This makes the four landed read-side fixes redundant safety nets rather than mandatory promotion paths, and prevents the same class of bug from recurring in future build paths.

Defense-in-depth (optional)

A startup-time migration that scans ~/.hermes/sessions/**/*.json and adds reasoning_content: "" (or copies from reasoning) on any assistant turn missing it would clean the existing fleet. I wrote one for my install — happy to PR it if useful. It found and repaired the 4 031 messages above; total run time on 1 497 files was under 10 seconds.

Workaround for affected users

Until the write side is fixed, two things have to be done together:

  1. hermes config set agent.reasoning_effort none (stops new poisoned writes when DeepSeek is primary)
  2. Run a one-time repair across the session store to inject reasoning_content: "" on every poisoned message — otherwise switching to DeepSeek at any later date re-triggers the 400.

(1) alone is not enough. (2) alone gets re-poisoned the next time a non-DeepSeek provider is used.

Environment

  • Hermes Agent v0.11.0 (2026.4.23)
  • Python 3.14.3
  • openai 2.26.0
  • macOS 26.4.1 (Darwin 25.4)
  • Provider: custom, base_url https://aigw.netease.com/v1
  • Affected models observed: deepseek-v4-pro (failing), glm-5.1 / MiniMax-M2.7 / gpt-5.4 / claude-opus-4-6 (poisoning sources)

Related

  • #15213 — main-loop reasoning_content guard (closed)
  • #15250 — initial DeepSeek V4 Flash session-poisoning report (closed)
  • #15353 — DeepSeek V4 thinking-mode tool-call 400 (closed)
  • #15407 — merged
  • #15478 — open
  • #15717 — generic 400 surface bug (closed)
  • #15741 — cron-path follow-up after #15213 closure (closed)
  • #15748 — _copy_reasoning_content_for_api ordering bug (closed)

All of the above are read-side fixes. This issue proposes a write-side fix that makes them unnecessary going forward.

extent analysis

TL;DR

The issue can be fixed by normalizing the field name at persistence time, writing the chain of thought to reasoning_content directly.

Guidance

  • Identify the point in run_agent.py where reasoning_text is finalized for the message and write msg["reasoning_content"] = _sanitize_surrogates(reasoning_text or "") unconditionally for assistant turns.
  • Consider keeping both reasoning and reasoning_content fields for one release for backward compatibility.
  • To clean the existing fleet, a startup-time migration can be implemented to scan and add reasoning_content: "" on any assistant turn missing it.
  • Until the write side is fixed, users can work around the issue by setting agent.reasoning_effort to none and running a one-time repair across the session store.

Example

msg["reasoning_content"] = _sanitize_surrogates(reasoning_text or "")

This code snippet writes the chain of thought to reasoning_content directly, which is the standard cross-provider name.

Notes

The proposed fix assumes that the reasoning_text variable is available and finalized at the point of writing the message. Additionally, the fix may require modifications to the read-side code to handle the new field name.

Recommendation

Apply the workaround by setting agent.reasoning_effort to none and running a one-time repair across the session store until the write side is fixed. This will prevent new poisoned writes and allow users to switch to DeepSeek without encountering the 400 error.

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