Skip to content

ai: Kimi K3 tool-loop 400 because reasoning_details replay includes streaming index #50232

Description

@vokasug

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

  1. Use provider kimi-code-plan-cn (Kimi coding plan), model k3, variant high (reasoning on).
  2. Send a prompt that forces a tool call, then a second model turn. Example: “read this image, then run a shell command, then finish.”
  3. First turn streams thinking and emits tool calls (read / shell). Tools complete.
  4. 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

  1. Stream parser accumulates delta.reasoning_details as-is. index is even used as a merge key (conflictingDetailValue(previous.index, current.index)).
  2. Finish metadata stores the array on the reasoning part (reasoningDetails).
  3. 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.

Activity

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