Bug Description
When using DeepSeek reasoning models (V4-Pro, R1, Reasoner) as the primary model, multi-turn conversations crash with HTTP 400 on the second turn and beyond:
Non-retryable client error: Error code: 400 - {'error': {'message':
'The reasoning_content in the thinking mode must be passed back to the API.'}}
Steps to Reproduce
- Configure DeepSeek V4 Pro as the primary model with base_url: https://api.deepseek.com/v1 (the correct endpoint — /anthropic has its own separate issue)
2. Start a conversation — turn 1 succeeds ✅
3. Send a second message in the same session — HTTP 400 ❌
Expected Behavior
This causes a chain failure:
1. Turn 1: DeepSeek returns reasoning_content → saved to DB ✅
2. Turn 2: session reloaded from DB → reasoning_content stripped ❌
3. Turn 2 runs: new response has reasoning_content, but prior assistant messages don't → HTTP 400
The _copy_reasoning_content_for_api() gap-fix in run_agent.py can't help here — it operates on api_messages, not the base history that gets built from DB reload.
Actual Behavior
On the first message in a session, everything works normally — DeepSeek responds with reasoning and a final answer. On the second message (same session), the agent crashes with a non-retryable HTTP 400:
Non-retryable client error: Error code: 400 - {'error': {'message':
'The reasoning_content in the thinking mode must be passed back to the API.',
'type': 'invalid_request_error', 'param': None, 'code': 'invalid_request_error'}}
The crash happens inside the gateway's agent loop. From ~/.hermes/logs/errors.log:
ERROR gateway.run:_run_agent:9847 — Non-retryable client error (HTTP 400):
{'error': {'message': 'The reasoning_content in the thinking mode must be
passed back to the API.', 'type': 'invalid_request_error', 'param': None,
'code': 'invalid_request_error'}}
The session is dead at this point — every subsequent message in that session produces the same 400 because prior assistant turns are missing reasoning_content and DeepSeek refuses to process a history where some assistant messages have it and others don't.
Affected Component
CLI (interactive chat)
Messaging Platform (if gateway-related)
No response
Debug Report
Environment
Repository: https://github.com/NousResearch/hermes-agent.git
Upstream HEAD: 627abbb1e (chore(release): map davidvv in AUTHOR_MAP)
Local changes: gateway/run.py (2-line fix), run_agent.py (gap-filler)
Provider: DeepSeek (custom, base_url: https://api.deepseek.com/v1)
Model: deepseek-v4-pro
Error Output
From ~/.hermes/logs/errors.log — multiple sessions, same crash pattern:
2026-04-25 10:23:30,754 ERROR [20260425_064607_5cbb93af] root:
Non-retryable client error: Error code: 400 -
{'error': {'message': 'The reasoning_content in the thinking mode
must be passed back to the API.', 'type': 'invalid_request_error',
'param': None, 'code': 'invalid_request_error'}}
2026-04-25 10:27:22,828 ERROR [20260425_064607_5cbb93af] root:
(same session, retry — same 400)
2026-04-25 10:30:09,120 ERROR [20260425_102739_d592f4f1] root:
(new session, second turn — same 400)
2026-04-27 08:05:38,043 WARNING [20260427_080149_e689a3] root:
Failed to get summary response: Error code: 400 -
'The reasoning_content in the thinking mode must be passed back to the API.'
2026-04-28 15:08:09,337 WARNING [20260428_142857_d1ae44] root:
Failed to get summary response: Error code: 400 -
'The reasoning_content in the thinking mode must be passed back to the API.'
Error hits both the main agent loop AND the session summarizer — because both go through _run_agent → agent_history rebuild.
The Broken Pipeline (Step by Step)
TURN 1:
DeepSeek API → returns reasoning_content: "Let me think..."
run_agent.py → stores in msg["reasoning_content"]
session.py → saves to SQLite ✅ (line 1151 — reasoning_content IS in schema)
session.py → writes to JSONL transcript ✅
TURN 2:
session.py → reads history from SQLite ✅ (line 1183 — reasoning_content IS loaded)
run.py:9940 → rebuilds agent_history from loaded messages
❌ DROPS reasoning_content — not in the preserved-fields tuple
agent_history → [{role:"user",...}, {role:"assistant", content:"...", reasoning:...}]
^ reasoning_content is ABSENT
run_agent.py → _copy_reasoning_content_for_api() gap-filler fires
BUT: it copies from msg["reasoning_content"] (which is missing)
→ nothing to copy, gap remains
DeepSeek API → sees: prior assistant turn WITHOUT reasoning_content
current turn WITH reasoning_content
→ HTTP 400: "reasoning_content must be passed back"
ALL SUBSEQUENT TURNS: same 400 — session is permanently broken
What Each File Does (and Doesn't Do)
| File | Saves reasoning_content? | Loads reasoning_content? | Rebuilds history with it? |
|---|---|---|---|
| gateway/session.py | ✅ line 1151 | ✅ line 1183 | N/A |
| gateway/run.py | ❌ line 9940 | N/A | ❌ dropped |
| run_agent.py | ✅ stores in msg dict | ✅ gap-filler at 7259 | ✅ but operates on already-broken agent_history |
The problem is in gateway/run.py alone. Session I/O is fine. The gap-filler in run_agent.py is correct but helpless — it receives history that was already stripped during the DB→agent_history rebuild.
The Fix (Unified Diff)
diff
--- a/gateway/run.py
+++ b/gateway/run.py
@@ -9937,10 +9937,11 @@
if role == "assistant":
- for _rkey in ("reasoning", "reasoning_details",
+ for _rkey in ("reasoning", "reasoning_content",
+ "reasoning_details",
"codex_reasoning_items"):
_rval = msg.get(_rkey)
- if _rval:
+ if _rval is not None:
entry[_rkey] = _rval
Change 1: Add "reasoning_content" — this is DeepSeek's native reasoning field, stored correctly by session.py but dropped during history rebuild.
Change 2: if _rval: → if _rval is not None: — the gap-filler in run_agent.py sets reasoning_content to "" on tool-call turns that lack it. if _rval evaluates "" as falsy and drops it again, causing the same 400 on the next turn. is not None preserves empty strings through the DB round-trip.
Reproducibility
100% reproducible. Any multi-turn DeepSeek session on unpatched main will crash on turn 2. The first message always succeeds (no prior history to violate), then the session is dead. Affects all DeepSeek reasoning models (V4-Pro, R1, Reasoner) via both /v1 and /anthropic endpoints.
Operating System
Ubuntu 24.04
Python Version
3.11
Hermes Version
No response
Additional Logs / Traceback (optional)
Root Cause Analysis (optional)
In gateway/run.py (~line 9939), when the gateway reloads session history from the database (_run_agent, building agent_history), it preserves reasoning fields on assistant messages — but reasoning_content is missing from the list:
python
Current upstream code (BROKEN):
if role == "assistant":
for _rkey in ("reasoning", # ← preserved
"reasoning_details", # ← preserved
"codex_reasoning_items"): # ← preserved
_rval = msg.get(_rkey)
if _rval: # ← falsy guard skips ""
entry[_rkey] = _rval
reasoning_content is SILENTLY DROPPED
This causes a chain failure:
1. Turn 1: DeepSeek returns reasoning_content → saved to DB ✅
2. Turn 2: session reloaded from DB → reasoning_content stripped ❌
3. Turn 2 runs: new response has reasoning_content, but prior assistant messages don't → HTTP 400
The _copy_reasoning_content_for_api() gap-fix in run_agent.py can't help here — it operates on api_messages, not the base history that gets built from DB reload.
Proposed Fix (optional)
Two changes in gateway/run.py:
python
FIXED:
if role == "assistant":
for _rkey in ("reasoning", "reasoning_content", # ← ADDED
"reasoning_details",
"codex_reasoning_items"):
_rval = msg.get(_rkey)
if _rval is not None: # ← was: if _rval:
entry[_rkey] = _rval
1. Add "reasoning_content" to the preserved fields tuple — this is DeepSeek's native reasoning field
2. Change if _rval: to if _rval is not None: — the gap-filler in run_agent.py sets reasoning_content to "" (empty string) on turns that lack it, and if _rval evaluates "" as falsy, dropping it again
Why is not None matters
DeepSeek requires reasoning_content to be present on every assistant message when the model is in thinking mode — even on tool-call turns where the field is empty string. The gap-fixer in run_agent.py fills missing ones with "", but without the is not None guard, empty strings are lost in the DB round-trip, causing the same 400.
Tested
Verified live against api.deepseek.com/v1:
| Scenario | Result |
|---|---|
| reasoning_content present on all turns | OK |
| reasoning_content missing on prior turns | HTTP 400 |
| Tool calls + reasoning_content | OK |
| Tool calls without reasoning_content | HTTP 400 |
Related
There's also a gap-filler in run_agent.py (_copy_reasoning_content_for_api()) that fills missing reasoning_content with "" on outgoing API messages. That fix is necessary but insufficient — it can't compensate for the DB reload stripping the field entirely from history.
Environment
- Hermes Agent version: up to current main
- Provider: DeepSeek (any reasoning model: V4-Pro, R1, Reasoner)
- Config: base_url: https://api.deepseek.com/v1, provider: custom
Are you willing to submit a PR for this?
Bug Description
When using DeepSeek reasoning models (V4-Pro, R1, Reasoner) as the primary model, multi-turn conversations crash with HTTP 400 on the second turn and beyond:
Steps to Reproduce
2. Start a conversation — turn 1 succeeds ✅
3. Send a second message in the same session — HTTP 400 ❌
Expected Behavior
This causes a chain failure:
Actual Behavior
On the first message in a session, everything works normally — DeepSeek responds with reasoning and a final answer. On the second message (same session), the agent crashes with a non-retryable HTTP 400:
Affected Component
CLI (interactive chat)
Messaging Platform (if gateway-related)
No response
Debug Report
Environment Repository: https://github.com/NousResearch/hermes-agent.git Upstream HEAD: 627abbb1e (chore(release): map davidvv in AUTHOR_MAP) Local changes: gateway/run.py (2-line fix), run_agent.py (gap-filler) Provider: DeepSeek (custom, base_url: https://api.deepseek.com/v1) Model: deepseek-v4-pro Error Output From ~/.hermes/logs/errors.log — multiple sessions, same crash pattern: 2026-04-25 10:23:30,754 ERROR [20260425_064607_5cbb93af] root: Non-retryable client error: Error code: 400 - {'error': {'message': 'The reasoning_content in the thinking mode must be passed back to the API.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_request_error'}} 2026-04-25 10:27:22,828 ERROR [20260425_064607_5cbb93af] root: (same session, retry — same 400) 2026-04-25 10:30:09,120 ERROR [20260425_102739_d592f4f1] root: (new session, second turn — same 400) 2026-04-27 08:05:38,043 WARNING [20260427_080149_e689a3] root: Failed to get summary response: Error code: 400 - 'The reasoning_content in the thinking mode must be passed back to the API.' 2026-04-28 15:08:09,337 WARNING [20260428_142857_d1ae44] root: Failed to get summary response: Error code: 400 - 'The reasoning_content in the thinking mode must be passed back to the API.' Error hits both the main agent loop AND the session summarizer — because both go through _run_agent → agent_history rebuild. The Broken Pipeline (Step by Step) TURN 1: DeepSeek API → returns reasoning_content: "Let me think..." run_agent.py → stores in msg["reasoning_content"] session.py → saves to SQLite ✅ (line 1151 — reasoning_content IS in schema) session.py → writes to JSONL transcript ✅ TURN 2: session.py → reads history from SQLite ✅ (line 1183 — reasoning_content IS loaded) run.py:9940 → rebuilds agent_history from loaded messages ❌ DROPS reasoning_content — not in the preserved-fields tuple agent_history → [{role:"user",...}, {role:"assistant", content:"...", reasoning:...}] ^ reasoning_content is ABSENT run_agent.py → _copy_reasoning_content_for_api() gap-filler fires BUT: it copies from msg["reasoning_content"] (which is missing) → nothing to copy, gap remains DeepSeek API → sees: prior assistant turn WITHOUT reasoning_content current turn WITH reasoning_content → HTTP 400: "reasoning_content must be passed back" ALL SUBSEQUENT TURNS: same 400 — session is permanently broken What Each File Does (and Doesn't Do) | File | Saves reasoning_content? | Loads reasoning_content? | Rebuilds history with it? | |---|---|---|---| | gateway/session.py | ✅ line 1151 | ✅ line 1183 | N/A | | gateway/run.py | ❌ line 9940 | N/A | ❌ dropped | | run_agent.py | ✅ stores in msg dict | ✅ gap-filler at 7259 | ✅ but operates on already-broken agent_history | The problem is in gateway/run.py alone. Session I/O is fine. The gap-filler in run_agent.py is correct but helpless — it receives history that was already stripped during the DB→agent_history rebuild. The Fix (Unified Diff) diff --- a/gateway/run.py +++ b/gateway/run.py @@ -9937,10 +9937,11 @@ if role == "assistant": - for _rkey in ("reasoning", "reasoning_details", + for _rkey in ("reasoning", "reasoning_content", + "reasoning_details", "codex_reasoning_items"): _rval = msg.get(_rkey) - if _rval: + if _rval is not None: entry[_rkey] = _rval Change 1: Add "reasoning_content" — this is DeepSeek's native reasoning field, stored correctly by session.py but dropped during history rebuild. Change 2: if _rval: → if _rval is not None: — the gap-filler in run_agent.py sets reasoning_content to "" on tool-call turns that lack it. if _rval evaluates "" as falsy and drops it again, causing the same 400 on the next turn. is not None preserves empty strings through the DB round-trip. Reproducibility 100% reproducible. Any multi-turn DeepSeek session on unpatched main will crash on turn 2. The first message always succeeds (no prior history to violate), then the session is dead. Affects all DeepSeek reasoning models (V4-Pro, R1, Reasoner) via both /v1 and /anthropic endpoints.Operating System
Ubuntu 24.04
Python Version
3.11
Hermes Version
No response
Additional Logs / Traceback (optional)
Root Cause Analysis (optional)
In gateway/run.py (~line 9939), when the gateway reloads session history from the database (_run_agent, building agent_history), it preserves reasoning fields on assistant messages — but reasoning_content is missing from the list:
Proposed Fix (optional)
Two changes in gateway/run.py:
Are you willing to submit a PR for this?