You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bug: DeepSeek v4 reasoning models return empty content through proxy (answer stuck in reasoning_content) #70
When SkillClaw proxies DeepSeek v4 reasoning models (deepseek-v4-flash / deepseek-v4-pro via the opencode.ai / zen/go upstream), the response arrives with content: "" and the actual answer stuffed into the non-standard reasoning_content field. Clients that read only content (e.g. Hermes agent) receive empty output and treat the call as a blank/failed completion.
This is not a silent degradation — every request through the proxy pays an extra round-trip and returns zero usable output; the client's fallback chain then re-issues the request to a direct provider, roughly doubling effective latency and cost.
Reproduction
Direct call to upstream (works, 1.5s, content populated):
POST https://opencode.ai/zen/go/v1/chat/completions
model: deepseek-v4-flash
-> content: "UPSTREAM" (non-empty)
Same call through the SkillClaw proxy (200 OK but empty content):
POST http://127.0.0.1:30000/v1/chat/completions
model: deepseek-v4-flash
-> 200 OK
message.content: ""
message.reasoning_content: "The user wants me to" <- actual output here
finish_reason: "length"
Root cause
skillclaw/api_server.py_handle_request() reads assistant_msg.get("content") but never checks reasoning_content. DeepSeek reasoning models return the visible output in reasoning_content with an empty content field.
Suggested fix
raw_content=assistant_msg.get("content")
# DeepSeek reasoning models return content in reasoning_content instead of contentreasoning_content=assistant_msg.get("reasoning_content") or""ifnotraw_contentandreasoning_content:
raw_content=reasoning_content
Environment
SkillClaw main (as of 2026-08-04)
Upstream: opencode.ai /zen/go/v1, model deepseek-v4-flash and deepseek-v4-pro
Hermes agent as the proxied client
We have removed SkillClaw from our stack pending these fixes (see also #52 for the event-loop freeze). Happy to provide more logs or test a patch.
The empty result is real, but copying reasoning_content into content is not a safe fix. For reasoning models that field can contain private chain-of-thought, and the sample also has finish_reason: "length", which indicates the model exhausted its output budget before producing the visible answer. Promoting the reasoning field would leak internal reasoning while masking the truncation.
I am keeping this open. Please attach one sanitized raw upstream JSON response and the corresponding response after SkillClaw, including the requested max_tokens / max_completion_tokens and final model ID. The correct fix should preserve reasoning as reasoning and surface/avoid the truncation, not relabel it as the answer. The separate proxy hang from oversized injected context was fixed in #80.
The problem is in skillclaw/api_server.py within the _handle_request() function. It only reads assistant_msg.get("content") and ignores reasoning_content. I will update the code to check reasoning_content when content is empty. This should ensure that the correct output is returned to clients like the Hermes agent. I will test the fix by verifying that the content field is populated correctly when using the proxy.
I will work on gathering the sanitized raw upstream JSON response and the corresponding response after SkillClaw. This will help ensure we address the issue without leaking internal reasoning or masking truncation. Once I have this data, I can proceed with implementing a fix that preserves reasoning and handles truncation appropriately.
@modelpath-dev Thanks for offering to collect a reproducer. To make the comparison actionable, please provide one minimal prompt with the same upstream endpoint/model and generation parameters, then the sanitized direct-upstream and proxied JSON responses. Include max_tokens/max_completion_tokens, finish_reason, usage counters, and whether skill injection was enabled. Please replace reasoning text with a placeholder plus its length; no private reasoning, prompts, keys or tokens are needed.
In particular, does finish_reason: length disappear when the output budget is raised with the same input? That distinguishes exhausted reasoning budget from a field-conversion bug. Keep this open pending that comparison; reasoning_content will not be copied into the visible answer.
A related, independently reproducible issue is fixed in #103: when assembling upstream SSE chunks, reasoning_content is now preserved separately instead of being dropped. Chat streaming also forwards those deltas live. This deliberately does not copy reasoning into visible content, and does not claim to resolve a provider response that ends with finish_reason: length before generating an answer. The direct/proxy comparison and token-budget details requested above are still needed to diagnose this report.
Activity
Upper9527 commentedon Aug 17, 2026
The empty result is real, but copying
reasoning_contentintocontentis not a safe fix. For reasoning models that field can contain private chain-of-thought, and the sample also hasfinish_reason: "length", which indicates the model exhausted its output budget before producing the visible answer. Promoting the reasoning field would leak internal reasoning while masking the truncation.I am keeping this open. Please attach one sanitized raw upstream JSON response and the corresponding response after SkillClaw, including the requested
max_tokens/max_completion_tokensand final model ID. The correct fix should preserve reasoning as reasoning and surface/avoid the truncation, not relabel it as the answer. The separate proxy hang from oversized injected context was fixed in #80.modelpath-dev commentedon Sep 10, 2026
I will take this issue. Please assign it to me.
The problem is in
skillclaw/api_server.pywithin the_handle_request()function. It only readsassistant_msg.get("content")and ignoresreasoning_content. I will update the code to checkreasoning_contentwhencontentis empty. This should ensure that the correct output is returned to clients like the Hermes agent. I will test the fix by verifying that thecontentfield is populated correctly when using the proxy.modelpath-dev commentedon Sep 13, 2026
I will work on gathering the sanitized raw upstream JSON response and the corresponding response after SkillClaw. This will help ensure we address the issue without leaking internal reasoning or masking truncation. Once I have this data, I can proceed with implementing a fix that preserves reasoning and handles truncation appropriately.
Upper9527 commentedon Oct 21, 2026
@modelpath-dev Thanks for offering to collect a reproducer. To make the comparison actionable, please provide one minimal prompt with the same upstream endpoint/model and generation parameters, then the sanitized direct-upstream and proxied JSON responses. Include
max_tokens/max_completion_tokens,finish_reason, usage counters, and whether skill injection was enabled. Please replace reasoning text with a placeholder plus its length; no private reasoning, prompts, keys or tokens are needed.In particular, does
finish_reason: lengthdisappear when the output budget is raised with the same input? That distinguishes exhausted reasoning budget from a field-conversion bug. Keep this open pending that comparison;reasoning_contentwill not be copied into the visible answer.Upper9527 commentedon Oct 21, 2026
A related, independently reproducible issue is fixed in #103: when assembling upstream SSE chunks,
reasoning_contentis now preserved separately instead of being dropped. Chat streaming also forwards those deltas live. This deliberately does not copy reasoning into visiblecontent, and does not claim to resolve a provider response that ends withfinish_reason: lengthbefore generating an answer. The direct/proxy comparison and token-budget details requested above are still needed to diagnose this report.