Skip to content

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

Closed

Description

@cjjdaq

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

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.

Activity

  1. yxl-lab commented on Aug 11, 2026

    @yxl-lab

    I reproduced this on the current dev branch: fromOpenaiRequest drops array-form output_text content from assistant messages, producing an assistant message without content. I’d like to fix the conversion while preserving tool calls and add a focused regression test. May I work on this?

  2. yxl-lab commented on Sep 22, 2026

    @yxl-lab

    Hi @cjjdaq — thanks for the update. I noticed this issue was closed as completed, while PR #41792 was closed by automated cleanup and was not merged. Could you share what resolved the issue (for example, a commit, another PR, or a Zen backend change)? This will help confirm whether #41792 is now obsolete. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions