Repository navigation
Kimi K3: HTTP 400 "assistant message must not be empty" when switching models mid-session #39451
Description
Description
Summary
When switching to Kimi K3 (Moonshot) mid-session, the next message fails with HTTP 400 because the replayed conversation history contains an assistant message with empty content (a tool-only turn). Kimi K3 rejects empty-content assistant messages; Kimi K2 and other OpenAI-compatible providers tolerate them.
Environment
- opencode:
1.18.9 - Provider: Moonshot AI (
/connect→ Moonshot AI) - Model: Kimi K3 — note: K3 is not yet in the providers docs (only K2 is documented at https://opencode.ai/docs/providers#moonshot-ai)
- OS:
macOS - Switched from: GLM-5.2
Reproduction
- Start a session with any model (e.g. GLM-5.2) and let the assistant perform several tool calls (bash/read/edit) — i.e. produce assistant turns that contain only
tool_callswith no text content. - Mid-session, switch the model to Kimi K3 via
/models. - Send any message (e.g. "test").
- Request fails immediately with HTTP 400.
Expected
The request is sent to Kimi K3 successfully, or opencode normalizes messages that Kimi K3 rejects.
Actual
HTTP 400 - {"error":{"message":"the message at position 31 with role 'assistant' must not be empty","type":"InvalidParameter","param":null,"code":"InvalidParameter"},"request_id":"9da933b5-c932-959d-b81c-5219bbe9e35f"}
The "position 31" assistant message is a tool-only turn (tool calls, no text). opencode serializes it with empty content, and Kimi K3 rejects empty-content assistant messages.
Analysis
- opencode (via the Vercel AI SDK) serializes assistant turns that contain only tool calls as messages with empty
content. - Kimi K3's API is stricter than Kimi K2 / OpenAI / GLM: it rejects assistant messages whose
contentis empty, even whentool_callsare present. - This only surfaces when switching models mid-session (full history replayed) or in long agentic sessions with many tool-only assistant turns.
- No config option exists to normalize/sanitize these messages — the
provideroptions (baseURL,blacklist/whitelist,options[timeout/chunkTimeout/setCacheKey/headers/apiKey],npm,name,models) include no message-normalization toggle (https://opencode.ai/docs/config).
Workarounds
/clearor start a new session before using Kimi K3 (history no longer contains the empty assistant turns).- Use Kimi K2 (documented, tolerates empty content) or another provider until K3 is officially supported.
Suggested fix
- Normalize assistant messages before sending to Kimi/Moonshot: when an assistant message has
tool_callsand emptycontent, sendcontent: null(OpenAI-compatible) or a single-space placeholder instead of an empty string — whichever Kimi K3 accepts. - Make this automatic for the Moonshot provider, or expose a per-provider option (e.g.
options.normalizeEmptyAssistantContent). - Add Kimi K3 to the providers docs / Models.dev metadata once stable.
References
- Providers docs: https://opencode.ai/docs/providers#moonshot-ai (only Kimi K2)
- Config docs: https://opencode.ai/docs/config
Plugins
No response
OpenCode version
1.18.9
Steps to reproduce
No response
Screenshot and/or share link
Operating System
macOs 26.5.1 (25F80)
Terminal
zsh
Activity
github-actions commentedon Jul 29, 2026
This issue might be a duplicate of existing issues. Please check:
If this is a distinct enough case (model-switch specifically), it may still warrant a separate fix, but coordinating with #37946 would help avoid duplicate work.
Arbeiter-bit commentedon Aug 19, 2026
I can reproduce the same issue on Windows with OpenCode Go and
kimi-k3.Environment
opencode-go/ Console Gokimi-k3https://opencode.ai/zen/go/v1/chat/completionsReproduction
kimi-k3.impptg commentedon Aug 31, 2026
Related failure family (I hit it with Kimi K3 behind a litellm gateway): assistant turns that replay with empty content get rejected by strict providers and brick the session.
A send-time workaround that may help depending on your exact shape: https://github.com/impptg/opencode-fix-empty-assistant-messages — a plugin hooking
experimental.chat.messages.transformthat drops emptytextparts and reasoning-only step segments from replayed history (the two shapes I confirmed producingcontent: ""on the wire). Tool-call-only turns are kept intact, so if your empty-content turn truly carriestool_calls, this plugin alone won't change that frame.The general fix is pending upstream in #45839 (filters assistant turns with no model-visible content at conversion time, which also heals already-bricked sessions).
neriousy commentedon Oct 1, 2026
Closing as a duplicate of #37946. Same root cause: an empty assistant message (here from a tool-only turn) is replayed and rejected with "must not be empty". Tracking in #37946.