Description
On OpenCode 2.0.11, a Kimi K3 coding-plan session with reasoning enabled dies on the second LLM request (right after the first tool calls). The provider returns:
provider.invalid-request (400)
the reasoning_details at position 2 entry 0 must not contain streaming index
The session then goes idle_outcome: failed. Same Kimi workflow worked on 1.18.31.
This is not a model refusal. OpenCode 2's new OpenAI-chat protocol stores SSE reasoning_details including the stream-only index field, then sends that array back on the tool-loop follow-up. Kimi rejects index on request messages.
Plugins
None.
OpenCode version
2.0.11 (OpenCode Desktop). Regression vs 1.18.31.
Steps to reproduce
- Use provider
kimi-code-plan-cn (Kimi coding plan), model k3, variant high (reasoning on).
- Send a prompt that forces a tool call, then a second model turn. Example: “read this image, then run a shell command, then finish.”
- First turn streams thinking and emits tool calls (
read / shell). Tools complete.
- OpenCode starts the follow-up request with the previous assistant message in history.
Screenshot and/or share link
UI after the failed turn:
- Thought block from the first turn is visible
- Tools completed (
read, sips)
- Red error under the tools:
the reasoning_details at position 2 entry 0 must not contain streaming index
- No further assistant text; session idle/failed
Stored reasoning on the failed session (redacted ciphertext):
{
"reasoningField": "reasoning_content",
"reasoningDetails": [
{ "index": 0, "type": "summary", "summary": "thinking..." },
{ "index": 1, "type": "encrypted", "encrypted": "<redacted>" }
]
}
position 2 is the assistant message with tool calls. entry 0 is the first details object, which still has "index": 0.
Operating System
macOS 26 (Darwin 25.6.0, arm64)
Terminal
OpenCode Desktop 2.0.11
Expected Behavior
After tools complete, the next Kimi request should succeed. Reasoning replay should be valid for a request body: reasoning_content and/or reasoning_details without SSE-only fields such as index.
That is what 1.18.31 did, and what the official api.moonshot.ai K3 fixture still does (reasoning_content string only).
Actual Behavior
Follow-up request is rejected with HTTP 400. Agent loop stops. Session idle_outcome: failed. Blocking for Kimi K3 coding-plan + reasoning + any tool use.
Root cause (1.18.31 vs 2.0.11)
v2 moved Kimi off AI SDK onto packages/ai OpenAI Chat.
1.18.31 packages/llm/src/protocols/openai-chat.ts
- Stream delta schema:
content, reasoning_content, tool_calls only. No reasoning_details.
- Assistant lowering sends only a string:
reasoning_content:
reasoning.length > 0
? reasoning.map((part) => part.text).join("")
: openAICompatibleReasoningContent(message.native?.openaiCompatible)
Follow-up body: { role, content, tool_calls, reasoning_content }. No details array, no index.
2.0.11 packages/ai/src/protocols/openai-chat.ts
- Stream parser accumulates
delta.reasoning_details as-is. index is even used as a merge key (conflictingDetailValue(previous.index, current.index)).
- Finish metadata stores the array on the reasoning part (
reasoningDetails).
lowerAssistantMessage replays it verbatim:
...(details !== undefined ? { reasoning_details: details } : {}),
There is no strip of stream-only fields before the next POST.
Official moonshot kimi-k3-preserves-reasoning-through-a-tool-loop-and-follow-up does not catch this: api.moonshot.ai streams only reasoning_content text, so the follow-up in that fixture never includes reasoning_details. Kimi coding-plan (kimi-code-plan-cn / k3 / high) streams summary + encrypted details with index.
Suggested fix
When lowering assistant messages (or when persisting stream details), drop SSE-only fields from each reasoning_details entry, at least index:
details.map(({ index, ...rest }) => rest)
Keep type / summary / encrypted (whatever the provider needs for replay). Add a recorded test against a coding-plan-style stream that emits reasoning_details[].index, then asserts the next request body has details without index.
Description
On OpenCode 2.0.11, a Kimi K3 coding-plan session with reasoning enabled dies on the second LLM request (right after the first tool calls). The provider returns:
The session then goes
idle_outcome: failed. Same Kimi workflow worked on 1.18.31.This is not a model refusal. OpenCode 2's new OpenAI-chat protocol stores SSE
reasoning_detailsincluding the stream-onlyindexfield, then sends that array back on the tool-loop follow-up. Kimi rejectsindexon request messages.Plugins
None.
OpenCode version
2.0.11 (OpenCode Desktop). Regression vs 1.18.31.
Steps to reproduce
kimi-code-plan-cn(Kimi coding plan), modelk3, varianthigh(reasoning on).read/shell). Tools complete.Screenshot and/or share link
UI after the failed turn:
read,sips)the reasoning_details at position 2 entry 0 must not contain streaming indexStored reasoning on the failed session (redacted ciphertext):
{ "reasoningField": "reasoning_content", "reasoningDetails": [ { "index": 0, "type": "summary", "summary": "thinking..." }, { "index": 1, "type": "encrypted", "encrypted": "<redacted>" } ] }position 2is the assistant message with tool calls.entry 0is the first details object, which still has"index": 0.Operating System
macOS 26 (Darwin 25.6.0, arm64)
Terminal
OpenCode Desktop 2.0.11
Expected Behavior
After tools complete, the next Kimi request should succeed. Reasoning replay should be valid for a request body:
reasoning_contentand/orreasoning_detailswithout SSE-only fields such asindex.That is what 1.18.31 did, and what the official
api.moonshot.aiK3 fixture still does (reasoning_contentstring only).Actual Behavior
Follow-up request is rejected with HTTP 400. Agent loop stops. Session
idle_outcome: failed. Blocking for Kimi K3 coding-plan + reasoning + any tool use.Root cause (1.18.31 vs 2.0.11)
v2 moved Kimi off AI SDK onto
packages/aiOpenAI Chat.1.18.31
packages/llm/src/protocols/openai-chat.tscontent,reasoning_content,tool_callsonly. Noreasoning_details.Follow-up body:
{ role, content, tool_calls, reasoning_content }. No details array, noindex.2.0.11
packages/ai/src/protocols/openai-chat.tsdelta.reasoning_detailsas-is.indexis even used as a merge key (conflictingDetailValue(previous.index, current.index)).reasoningDetails).lowerAssistantMessagereplays it verbatim:There is no strip of stream-only fields before the next POST.
Official moonshot
kimi-k3-preserves-reasoning-through-a-tool-loop-and-follow-updoes not catch this:api.moonshot.aistreams onlyreasoning_contenttext, so the follow-up in that fixture never includesreasoning_details. Kimi coding-plan (kimi-code-plan-cn/k3/high) streamssummary+encrypteddetails withindex.Suggested fix
When lowering assistant messages (or when persisting stream details), drop SSE-only fields from each
reasoning_detailsentry, at leastindex:Keep
type/summary/encrypted(whatever the provider needs for replay). Add a recorded test against a coding-plan-style stream that emitsreasoning_details[].index, then asserts the next request body has details withoutindex.