Hmbown/CodeWhale · error · anyhow::Error
Kimi returned an empty native web-search tool call list
Error message
Kimi returned an empty native web-search tool call list
What it means
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.
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
Example fix
// before: hard bail on empty list
if tool_calls.is_empty() {
bail!("Kimi returned an empty native web-search tool call list");
}
// after: treat as final text answer instead of failing the request
if tool_calls.is_empty() {
return Ok(ProviderNativeSearchResponse::text_fallback(message));
} Defensive patterns
Strategy: try-catch
Validate before calling
// before the search request, confirm the model supports Kimi native web search
assert!(model_id.starts_with("kimi"), "native $web_search requires a Kimi model"); Type guard
fn tool_calls_valid(msg: &Value) -> bool {
msg.get("tool_calls").and_then(Value::as_array).map_or(false, |t| !t.is_empty())
} Try / catch
match provider.search(req).await {
Err(e) if e.to_string().contains("empty native web-search tool call list") => {
log::warn!("Kimi returned no search calls; falling back");
fallback_search(req).await
}
other => other,
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- Kimi Formula web search exceeded the bounded tool-call loop
- Kimi Formula web search requested an unexpected tool
- Kimi native search requested an unexpected tool
- Kimi native web search exceeded the bounded tool-call loop
- Kimi native web search exceeded the
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/e85d933af848784f.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/client/provider_native_search/kimi.rs:100
let payload = client.post_json(&url, &body, &[]).await?;
let choice = payload
.pointer("/choices/0")
.context("Kimi web search response omitted choices[0]")?;
let message = choice
.get("message")
.and_then(Value::as_object)
.context("Kimi web search response omitted assistant message")?;
if choice.get("finish_reason").and_then(Value::as_str) != Some("tool_calls") {
return Ok(parse_final_message(message));
}
messages.push(Value::Object(message.clone()));
let tool_calls = message
.get("tool_calls")
.and_then(Value::as_array)
.context("Kimi returned tool_calls finish reason without tool calls")?;
if tool_calls.is_empty() {
bail!("Kimi returned an empty native web-search tool call list");
}
reserve_native_search_tool_calls(&mut tool_calls_executed, tool_calls.len())?;
for tool_call in tool_calls {
if tool_call.pointer("/function/name").and_then(Value::as_str) != Some("$web_search") {
bail!("Kimi native search requested an unexpected tool");
}
let id = tool_call
.get("id")
.and_then(Value::as_str)
.context("Kimi native web-search call omitted id")?;
let arguments = tool_call
.pointer("/function/arguments")
.and_then(Value::as_str)
.context("Kimi native web-search call omitted arguments")?;
let _: Value = serde_json::from_str(arguments)
.context("Kimi native web-search arguments were not valid JSON")?;
messages.push(json!({
"role": "tool",View on GitHub (pinned to 73e0f67d83)