anomalyco / anomalyco/opencode
Zen /v1/responses drops array-format assistant content -> 400 "Invalid assistant message: content or tool_calls must be set"
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Summary
The Zen endpoint (https://opencode.ai/zen/v1/responses) rejects standard Responses API requests whose assistant message content is an array of content parts (e.g. [{"type":"output_text","text":"..."}]). It returns:
400 invalid_request_error: Error from provider (Console): Upstream request failed: [invalid_request_error] Invalid assistant message: content or tool_calls must be set
The same request works fine when the assistant content is a plain string. This affects all models on /zen (verified with deepseek-v4-flash-free and mimo-v2.5-free) and breaks any standards-compliant Responses API client (OpenAI SDK, Hermes, Codex-style clients, cc-switch + Claude Code).
Steps to Reproduce
Send this minimal request to https://opencode.ai/zen/v1/responses with any Zen API key:
{
"model": "deepseek-v4-flash-free",
"input": [
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "hi"}]},
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "hello"}]},
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "again?"}]}
],
"max_output_tokens": 20
}
Result: HTTP 400 Invalid assistant message: content or tool_calls must be set
Change only the assistant content to a plain string:
{"type": "message", "role": "assistant", "content": "hello"}
Result: HTTP 200 OK.
Expected Behavior
The Responses API spec explicitly allows assistant message content as an array of content parts (output_text, refusal, etc.). The Zen /v1/responses endpoint should accept the same format that the official OpenAI Responses API accepts — the array format is what every Responses-API SDK sends on multi-turn history replay.
Actual Behavior
Array-format assistant content is silently dropped before reaching the model, producing an empty assistant message, which the upstream model validator then rejects with content or tool_calls must be set.
Root Cause (source reference)
In packages/console/app/src/routes/zen/util/provider/openai.ts, the assistant branch only copies content when it is a string:
if ((m as any).role === "assistant") {
const c = (m as any).content
const out: any = { role: "assistant" }
if (typeof c === "string" && c.length > 0) out.content = c // array content is dropped
if (Array.isArray((m as any).tool_calls)) out.tool_calls = (m as any).tool_calls
msgs.push(out)
continue
}
Note the user branch above it does handle arrays (text / input_text / image parts), so this looks like an oversight in the assistant branch specifically: it should extract text from output_text parts (or pass the array through) instead of dropping it.
Impact
- All Zen models on
/zen(free tier:deepseek-v4-flash-free,mimo-v2.5-free; likely all others too, since it's a shared code path) - Any third-party client using the standard Responses API against Zen: OpenAI SDKs, Hermes Agent, Codex/cc-switch, Cherry Studio Claude Code mode, etc.
- The
/zen/goendpoint (different implementation) handles array content correctly, so this is/zen-specific
Related Issues / PRs
- #32821 — GLM-5.2 via OpenCode Go:
messages.content should be a valid string(same root cause family: array content rejected) - #24090 — assistant messages in history replay missing
tool_callsfield (conversion drops assistant fields) - PR #27914 — filter empty assistant content for openai-compatible (related guard)
- #15375 — empty tool_result content guard in Anthropic provider (same "empty content gets rejected" class)
Workaround (for affected users)
Convert the responses request to chat completions before hitting Zen (any relay/gateway can do this), or flatten assistant content arrays to plain strings client-side. Neither is ideal — this should be fixed server-side.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/console/app/src/routes/zen/util/provider/openai.ts and inspect the assistant branch that only copies string content. Reproduce the documented /zen/v1/responses request, then ensure assistant content arrays containing output_text are preserved or converted into usable text while tool_calls continue to work; the array-format request should return 200 like the string variant.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100