{"record":{"id":"e85d933af848784f","repo":"Hmbown/CodeWhale","slug":"kimi-returned-an-empty-native-web-search-tool-call-list","errorCode":null,"errorMessage":"Kimi returned an empty native web-search tool call list","messagePattern":"Kimi returned an empty native web-search tool call list","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/client/provider_native_search/kimi.rs","lineNumber":100,"sourceCode":"        let payload = client.post_json(&url, &body, &[]).await?;\n        let choice = payload\n            .pointer(\"/choices/0\")\n            .context(\"Kimi web search response omitted choices[0]\")?;\n        let message = choice\n            .get(\"message\")\n            .and_then(Value::as_object)\n            .context(\"Kimi web search response omitted assistant message\")?;\n        if choice.get(\"finish_reason\").and_then(Value::as_str) != Some(\"tool_calls\") {\n            return Ok(parse_final_message(message));\n        }\n\n        messages.push(Value::Object(message.clone()));\n        let tool_calls = message\n            .get(\"tool_calls\")\n            .and_then(Value::as_array)\n            .context(\"Kimi returned tool_calls finish reason without tool calls\")?;\n        if tool_calls.is_empty() {\n            bail!(\"Kimi returned an empty native web-search tool call list\");\n        }\n        reserve_native_search_tool_calls(&mut tool_calls_executed, tool_calls.len())?;\n        for tool_call in tool_calls {\n            if tool_call.pointer(\"/function/name\").and_then(Value::as_str) != Some(\"$web_search\") {\n                bail!(\"Kimi native search requested an unexpected tool\");\n            }\n            let id = tool_call\n                .get(\"id\")\n                .and_then(Value::as_str)\n                .context(\"Kimi native web-search call omitted id\")?;\n            let arguments = tool_call\n                .pointer(\"/function/arguments\")\n                .and_then(Value::as_str)\n                .context(\"Kimi native web-search call omitted arguments\")?;\n            let _: Value = serde_json::from_str(arguments)\n                .context(\"Kimi native web-search arguments were not valid JSON\")?;\n            messages.push(json!({\n                \"role\": \"tool\",","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/client/provider_native_search/kimi.rs#L82-L118","documentation":"Kimi's native web-search integration requested a continuation because the model returned a finish_reason of tool_calls, but the assistant message carried an empty (or missing-after-check) tool_calls array. The provider contract requires at least one $web_search tool call in that round; an empty list means the upstream response is malformed relative to the finish reason, so the client bails instead of looping on nothing.","triggerScenarios":"During search_builtin, the streamed Kimi response ends with finish reason 'tool_calls' but message.tool_calls is present yet an empty JSON array (after passing the earlier .context check for a missing field).","commonSituations":"Kimi API behavior changes or regressions; a proxy/gateway stripping tool_calls contents; an unexpected model (non-Kimi or new Kimi version) routed at the Kimi endpoint that emits tool_calls finish reason without calls.","solutions":["Inspect the raw Kimi response body to confirm tool_calls is genuinely empty and capture it for a bug report","Verify the configured model actually supports the $web_search native tool (e.g. kimi/k2 variants with web search enabled)","Check for a proxy or API gateway that mutates/strips tool_calls from responses","Pin/adjust the Kimi model or API version and retry; if upstream persists, handle the error and fall back to non-native search"],"exampleFix":"// before: hard bail on empty list\nif tool_calls.is_empty() {\n    bail!(\"Kimi returned an empty native web-search tool call list\");\n}\n// after: treat as final text answer instead of failing the request\nif tool_calls.is_empty() {\n    return Ok(ProviderNativeSearchResponse::text_fallback(message));\n}","handlingStrategy":"try-catch","validationCode":"// before the search request, confirm the model supports Kimi native web search\nassert!(model_id.starts_with(\"kimi\"), \"native $web_search requires a Kimi model\");","typeGuard":"fn tool_calls_valid(msg: &Value) -> bool {\n    msg.get(\"tool_calls\").and_then(Value::as_array).map_or(false, |t| !t.is_empty())\n}","tryCatchPattern":"match provider.search(req).await {\n    Err(e) if e.to_string().contains(\"empty native web-search tool call list\") => {\n        log::warn!(\"Kimi returned no search calls; falling back\");\n        fallback_search(req).await\n    }\n    other => other,\n}","preventionTips":["Pin to Kimi model versions known to support $web_search","Log raw responses from the search path to catch upstream regressions early","Route around proxies that may mutate tool_calls"],"tags":["kimi","web-search","upstream-api","empty-response"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}