Hmbown/CodeWhale · error · anyhow::Error
Moonshot cannot force tool
Error message
Moonshot cannot force tool '{name}' because its input schema is incompatible with this route What it means
When the chat route's tool filter omitted a tool (because its input schema is not representable on this wire), a forced `tool_choice` naming that tool would ask Moonshot to call a tool that was never advertised — which the route rejects. `build_chat_wire_body` detects this conflict and fails loudly instead of sending a broken body. Raised in `crates/tui/src/client/chat.rs` around line 1215.
Solutions
- Remove or relax the forced `tool_choice` to `auto`/`none` so the omitted tool isn't demanded.
- Simplify the tool's input schema so it survives the route's tool filtering and is actually advertised.
- Use a different provider/route whose wire format supports the tool's schema.
- If the tool is essential, register a schema-compatible variant of it for Moonshot routes.
Example fix
// before
body["tool_choice"] = json!({"type": "function", "function": {"name": "run_query"}});
// after
body["tool_choice"] = json!("auto"); // run_query was omitted on this Moonshot route Defensive patterns
Strategy: validation
Validate before calling
fn forced_tool_available(choice: &Value, advertised: &[&str]) -> Result<(), String> {
if let Some(name) = choice.pointer("/function/name").and_then(Value::as_str) {
if !advertised.contains(&name) {
return Err(format!("tool '{name}' was filtered out; cannot force it"));
}
}
Ok(())
} Type guard
fn is_forced_named_tool(tool_choice: &Value) -> Option<&str> {
tool_choice.pointer("/function/name").and_then(Value::as_str)
} Try / catch
match build_chat_wire_body(...) {
Err(e) if e.to_string().contains("cannot force tool") => {
body["tool_choice"] = json!("auto"); // degrade instead of failing
retry_request(body)
}
other => other,
} Prevention
- Check whether a forced tool survived the route's schema filtering before pinning tool_choice.
- Simplify tool input schemas for Moonshot routes.
- Fall back to tool_choice:auto when a named tool was omitted.
When it happens
Trigger: Building a chat request for provider Moonshot with `tool_choice` mapped to a specific function whose name appears in `omitted_tool_names` — i.e. `map_tool_choice_for_chat` produced `{"function":{"name":...}}` for a tool that was dropped from `body["tools"]` due to schema incompatibility.
Common situations: A tool whose JSON schema uses constructs Moonshot's route can't encode (e.g. complex anyOf/nested formats) combined with an explicit 'force tool X' request; config where tool_choice is pinned while tool filtering silently removed the tool.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- Antigravity cloud-code tools are not implemented yet; send…
- Automation run schema v
- Automation schema v is newer than supported v
- Checkpoint schema v is newer than supported v
- Cloud agent create succeeded but returned no usable sandbox…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/d9ba4f25b04b2da4.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/client/chat.rs:1215
}
}
}
// When per-tool degradation (or the caller) left no tools, omit the
// key entirely: an empty `tools` array — or a `tool_choice` pointing
// at a dropped tool — is itself a fresh 400 on strict providers.
if !chat_tools.is_empty() {
body["tools"] = json!(chat_tools);
}
}
if should_send_tool_choice_for_chat(provider, request.reasoning_effort.as_deref())
&& let Some(choice) = request.tool_choice.as_ref()
&& let Some(mapped) = map_tool_choice_for_chat(choice)
{
if matches!(provider, crate::config::ApiProvider::Moonshot)
&& let Some(name) = mapped.pointer("/function/name").and_then(Value::as_str)
&& omitted_tool_names.iter().any(|omitted| omitted == name)
{
bail!(
"Moonshot cannot force tool '{name}' because its input schema is incompatible with this route"
);
}
if body.get("tools").is_some() {
body["tool_choice"] = mapped;
}
}
apply_route_reasoning_controls(
&mut body,
provider,
base_url,
&model,
request.reasoning_effort.as_deref(),
);
apply_direct_moonshot_k3_fixed_sampling(&mut body, provider, base_url, &model);
apply_kimi_code_fixed_sampling(&mut body, provider, base_url, &model);
// Bulletproof final sanitizer: walk the wire payload and forceView on GitHub (pinned to 73e0f67d83)