Skip to content

Kimi: multi-part content serializes without a type field, breaking subagents and multimodal messages #98

Description

@flaviomartil

Summary

The Kimi provider serializes multi-part content without a type field, so Kimi rejects the request with invalid part type: (note the empty value after the colon). This affects both tool results and multimodal user messages.

The most visible symptom is that subagents are unusable: Claude Code's Task tool returns a multi-block tool_result, which always trips this path.

Reproduction

Both against a running claude-code-proxy serve with Kimi authenticated.

1. Multimodal user message

curl -s http://127.0.0.1:18765/v1/messages -H 'Content-Type: application/json' -d '{
  "model": "k3", "max_tokens": 16,
  "messages": [{"role": "user", "content": [
    {"type": "text", "text": "what color?"},
    {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "<1x1 png>"}}
  ]}]}'
{"error":{"message":"the message at position 0 with role 'user' contains an invalid part type: ","type":"invalid_request_error"}}

2. Tool result with more than one part (what the Task tool produces)

Running any Claude Code prompt that spawns a subagent through the proxy:

API Error: 502 {"error":{"message":"the message at position 2 with role 'tool' contains an invalid part type: ","type":"invalid_request_error"}}

Reproduced on both k3 and kimi-for-coding, so it is not model specific.

Root cause

src/providers/kimi/translate/request.rs:

#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
enum KimiUserContentPart {
    Text { text: String },
    ImageUrl { image_url: KimiImageUrl },
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
enum KimiToolResultPart {
    Text { text: String },
    ImageUrl { image_url: KimiImageUrl },
}

#[serde(untagged)] drops the discriminant, so these serialize as {"text": "..."} and {"image_url": {...}}. Kimi expects the OpenAI multimodal shape, {"type": "text", "text": "..."} and {"type": "image_url", "image_url": {...}}, and reports the missing discriminant as an empty part type.

Why single-part tool results still work

tool_result_content collapses a lone text part back into a plain string before serializing:

// Collapse to string when only one text part
if parts.len() == 1
    && let KimiToolResultPart::Text { text } = &parts[0]
{
    return Value::String(text.clone());
}

Plain strings are valid, so ordinary single-output tools (Bash, Read, Edit) never hit the bug. Anything that produces two or more parts does.

Proposed fix

Tag both enums:

#[serde(tag = "type", rename_all = "snake_case")]

That emits {"type":"text","text":"..."} and {"type":"image_url","image_url":{...}}.

The existing tests translate_tool_result_with_unsupported_blocks and translate_tool_result_with_image assert only on the text and image_url keys, so they keep passing.

Environment

  • claude-code-proxy 0.1.30 (Homebrew, darwin-arm64)
  • macOS 15 (Darwin 24.6.0), Apple M4
  • Claude Code 2.1.220
  • Provider: kimi, models k3 and kimi-for-coding

Happy to send a PR.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions