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.
Summary
The Kimi provider serializes multi-part content without a
typefield, so Kimi rejects the request withinvalid 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
Tasktool returns a multi-blocktool_result, which always trips this path.Reproduction
Both against a running
claude-code-proxy servewith Kimi authenticated.1. Multimodal user message
{"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
Tasktool produces)Running any Claude Code prompt that spawns a subagent through the proxy:
Reproduced on both
k3andkimi-for-coding, so it is not model specific.Root cause
src/providers/kimi/translate/request.rs:#[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_contentcollapses a lone text part back into a plain string before serializing: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_blocksandtranslate_tool_result_with_imageassert only on thetextandimage_urlkeys, so they keep passing.Environment
k3andkimi-for-codingHappy to send a PR.