Summary
Long agentic sessions against DeepSeek via the opencode relay (opencode/deepseek-v4-flash-free) fail with HTTP 400 Invalid assistant message: content or tool_calls must be set after updating to next-16998+. The relay/serializer appears to emit an assistant message with the content key missing (not merely empty), which DeepSeek rejects. The identical session history runs cleanly through OpenRouter's DeepSeek endpoint, so this is specific to the opencode relay's DeepSeek serialization path. This is a regression — the same sessions worked on next-16974.
Environment
- opencode version: 0.0.0-next-17002 (first failing build observed: 0.0.0-next-16998; last known-good: 0.0.0-next-16974)
- OS: Darwin 27.0.0 (darwin arm64)
- Terminal: TERM_PROGRAM=ghostty, TERM=xterm-ghostty, COLORTERM=truecolor
- Shell: /bin/zsh
- Install/channel: next (npm
@opencode-ai/cli)
- Active plugins:
confirmo-plugin.ts, done-sound.js, herdr-agent-state.js, taskmaster.js
- Model:
opencode/deepseek-v4-flash-free (the opencode relay's alias for deepseek-v4-flash)
Reproduction
- Run a long agentic session on
opencode/deepseek-v4-flash-free that accumulates many steps including reasoning-only assistant turns (ideally with at least one auto-compaction).
- Continue the session (e.g.
POST /api/session/{id}/prompt with {"text":"continue"}).
- Every subsequent provider request fails with the same 400.
Minimal API repro on an affected session:
opencode2 api post /api/session/ses_0256611d1ffezr3HNUHWLSf3uJ/prompt --data '{"text":"continue"}'
Expected Behavior
The session continues normally. The relay should not send an assistant message with a missing content key to DeepSeek — reasoning-only or otherwise-empty assistant turns should be skipped, merged, or given a content value before sending.
Actual Behavior
Every provider request fails:
AI.Error: RequestExecutor.execute: Provider request failed with HTTP 400:
{"error":{"message":"Invalid assistant message: content or tool_calls must be set","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}
A second affected session (4 auto-compactions, 194 assistant messages) produced the related variant:
HTTP 400: {"error":{"type":"invalid_request_error","message":"the message at position 159 with role 'assistant' must not be empty"}}
Stack (top frames):
at <anonymous> (../ai/src/route/executor.ts:232:25)
at SessionRunner.callModel (../core/src/session/runner/llm.ts:187:32)
at SessionRunner.runSteps (../core/src/session/runner/llm.ts:129:16)
Root-cause isolation (direct DeepSeek API tests)
I tested DeepSeek's API directly (https://api.deepseek.com/v1/chat/completions) to pin down exactly what it rejects:
| Payload |
DeepSeek response |
{"role":"assistant","content":""} (empty string) |
✅ accepted |
{"role":"assistant"} (content key missing) |
❌ exact error above |
{"role":"assistant","content":null} |
❌ exact error above |
{"role":"assistant","reasoning_content":"..."} (no content) |
❌ exact error above |
reasoning_content + content both present |
✅ accepted |
So DeepSeek requires the content key to be present and non-null (or tool_calls) on every assistant message — stricter than OpenAI. The opencode relay is emitting an assistant message with content missing or null (most likely a reasoning-only step serialized as {"role":"assistant","reasoning_content":"..."} with no content).
Cross-check: the same failing sessions, switched to openrouter/deepseek/deepseek-v4-flash (OpenRouter's DeepSeek endpoint), drained cleanly with zero errors — confirming the malformed payload is specific to the opencode relay path, not the underlying model.
Additional Context
- Regression window: sessions worked at 2026-08-07 ~13:47 UTC on
next-16974; first failure at 19:19 UTC on next-16998. Persists through next-17002.
- Not a data problem: inspected the local SQLite store for the affected session — every assistant message has non-empty text or tool parts; no empty assistant message is stored. Parts breakdown:
reasoning 28, step-start 32, step-finish 31, text 24, tool 38.
- Likely cause: the empty message is created at serialization time, not stored. The 28
reasoning parts are the prime suspect — a reasoning-only step likely serializes to {role:"assistant"} without content/tool_calls for the DeepSeek/OpenAI-compatible path.
- Not config-related: user config only sets
model, default_agent, permissions, lsp, mcp.servers, and two unrelated custom providers (cloudflare-workers-ai, meta). No custom serialization config.
- Scope: 4 sessions affected; the 2 still in the DB reproduced 100% of the time after a service restart. Same window also surfaced
Failed to read deepseek/openai-compatible-chat stream and Failed to read opencode/openai-compatible-chat stream errors, suggesting a shared OpenAI-compatible serialization path changed.
- Impact: affected sessions become unrecoverable via the UI/API against the
opencode relay — every retry replays the same invalid history. Recoverable by switching the session to openrouter/deepseek/deepseek-v4-flash (e.g. POST /api/session/{id}/model) and resuming with opencode2 run --session <id> --model openrouter/deepseek/deepseek-v4-flash "continue".
- Workaround: use OpenRouter's DeepSeek endpoint instead of the
opencode relay for DeepSeek models.
Summary
Long agentic sessions against DeepSeek via the
opencoderelay (opencode/deepseek-v4-flash-free) fail with HTTP 400Invalid assistant message: content or tool_calls must be setafter updating tonext-16998+. The relay/serializer appears to emit an assistant message with thecontentkey missing (not merely empty), which DeepSeek rejects. The identical session history runs cleanly through OpenRouter's DeepSeek endpoint, so this is specific to theopencoderelay's DeepSeek serialization path. This is a regression — the same sessions worked onnext-16974.Environment
@opencode-ai/cli)confirmo-plugin.ts,done-sound.js,herdr-agent-state.js,taskmaster.jsopencode/deepseek-v4-flash-free(theopencoderelay's alias fordeepseek-v4-flash)Reproduction
opencode/deepseek-v4-flash-freethat accumulates many steps including reasoning-only assistant turns (ideally with at least one auto-compaction).POST /api/session/{id}/promptwith{"text":"continue"}).Minimal API repro on an affected session:
opencode2 api post /api/session/ses_0256611d1ffezr3HNUHWLSf3uJ/prompt --data '{"text":"continue"}'Expected Behavior
The session continues normally. The relay should not send an assistant message with a missing
contentkey to DeepSeek — reasoning-only or otherwise-empty assistant turns should be skipped, merged, or given acontentvalue before sending.Actual Behavior
Every provider request fails:
A second affected session (4 auto-compactions, 194 assistant messages) produced the related variant:
Stack (top frames):
Root-cause isolation (direct DeepSeek API tests)
I tested DeepSeek's API directly (
https://api.deepseek.com/v1/chat/completions) to pin down exactly what it rejects:{"role":"assistant","content":""}(empty string){"role":"assistant"}(contentkey missing){"role":"assistant","content":null}{"role":"assistant","reasoning_content":"..."}(nocontent)reasoning_content+contentboth presentSo DeepSeek requires the
contentkey to be present and non-null (ortool_calls) on every assistant message — stricter than OpenAI. Theopencoderelay is emitting an assistant message withcontentmissing or null (most likely a reasoning-only step serialized as{"role":"assistant","reasoning_content":"..."}with nocontent).Cross-check: the same failing sessions, switched to
openrouter/deepseek/deepseek-v4-flash(OpenRouter's DeepSeek endpoint), drained cleanly with zero errors — confirming the malformed payload is specific to theopencoderelay path, not the underlying model.Additional Context
next-16974; first failure at 19:19 UTC onnext-16998. Persists throughnext-17002.reasoning28,step-start32,step-finish31,text24,tool38.reasoningparts are the prime suspect — a reasoning-only step likely serializes to{role:"assistant"}withoutcontent/tool_callsfor the DeepSeek/OpenAI-compatible path.model,default_agent,permissions,lsp,mcp.servers, and two unrelated customproviders(cloudflare-workers-ai, meta). No custom serialization config.Failed to read deepseek/openai-compatible-chat streamandFailed to read opencode/openai-compatible-chat streamerrors, suggesting a shared OpenAI-compatible serialization path changed.opencoderelay — every retry replays the same invalid history. Recoverable by switching the session toopenrouter/deepseek/deepseek-v4-flash(e.g.POST /api/session/{id}/model) and resuming withopencode2 run --session <id> --model openrouter/deepseek/deepseek-v4-flash "continue".opencoderelay for DeepSeek models.