anomalyco / anomalyco/opencode

Zen /v1/responses drops array-format assistant content -> 400 "Invalid assistant message: content or tool_calls must be set"

Open Beginner friendly
#41,766 2 comments 1 reaction 0 assignees View on GitHub

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/go endpoint (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_calls field (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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.