Repository navigation
Description
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(theopencoderelay's alias fordeepseek-v4-flash)
Reproduction
- Run a long agentic session on
opencode/deepseek-v4-flash-freethat accumulates many steps including reasoning-only assistant turns (ideally with at least one auto-compaction). - Continue the session (e.g.
POST /api/session/{id}/promptwith{"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 onnext-16998. Persists throughnext-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:
reasoning28,step-start32,step-finish31,text24,tool38. - Likely cause: the empty message is created at serialization time, not stored. The 28
reasoningparts are the prime suspect — a reasoning-only step likely serializes to{role:"assistant"}withoutcontent/tool_callsfor the DeepSeek/OpenAI-compatible path. - Not config-related: user config only sets
model,default_agent,permissions,lsp,mcp.servers, and two unrelated customproviders(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 streamandFailed to read opencode/openai-compatible-chat streamerrors, suggesting a shared OpenAI-compatible serialization path changed. - Impact: affected sessions become unrecoverable via the UI/API against the
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". - Workaround: use OpenRouter's DeepSeek endpoint instead of the
opencoderelay for DeepSeek models.
Activity
github-actions commentedon Aug 8, 2026
This issue might be a duplicate of existing issues. Please check:
content: []orcontent: nullthat providers reject. The issue specifically calls out the interleaved reasoning filter as one culprit.reasoning_contenthandling in DeepSeek interleaved mode causing malformed assistant messages across turns.opencode-agent commentedon Aug 8, 2026
Fixed by #41210.