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.

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