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

  1. Remove or relax the forced `tool_choice` to `auto`/`none` so the omitted tool isn't demanded.
  2. Simplify the tool's input schema so it survives the route's tool filtering and is actually advertised.
  3. Use a different provider/route whose wire format supports the tool's schema.
  4. 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

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


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 force

View on GitHub (pinned to 73e0f67d83)