zeroclaw-labs/zeroclaw · error · anyhow::Error

Invalid OpenAI tool specification: unsupported tool type '{}

Error message

Invalid OpenAI tool specification: unsupported tool type '{}', expected 'function'

What it means

parse_native_tool_spec deserializes a caller-supplied tool definition in OpenAI chat-completions wire format and then enforces that the top-level `type` field equals "function" — the only tool type OpenAI chat completions accept. Any other value ("custom", "code_interpreter", "local_shell", or a misspelling) is rejected before a request is ever built. The spec must be {"type":"function","function":{"name","description","parameters"}}.

Source

Thrown at crates/zeroclaw-providers/src/openai.rs:160

    /// built specs.
    #[serde(flatten)]
    pub(crate) extra: serde_json::Map<String, serde_json::Value>,
}

pub(crate) fn parse_native_tool_spec(value: serde_json::Value) -> anyhow::Result<NativeToolSpec> {
    let spec: NativeToolSpec = serde_json::from_value(value).map_err(|e| {
        ::zeroclaw_log::record!(
            WARN,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
            "openai: invalid tool spec"
        );
        anyhow::Error::msg(format!("Invalid OpenAI tool specification: {e}"))
    })?;

    if spec.kind != "function" {
        anyhow::bail!(
            "Invalid OpenAI tool specification: unsupported tool type '{}', expected 'function'",
            spec.kind
        );
    }

    Ok(spec)
}

#[derive(Debug, Serialize, Deserialize)]
struct NativeToolCall {
    #[serde(skip_serializing_if = "Option::is_none")]
    id: Option<String>,
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    kind: Option<String>,
    function: NativeFunctionCall,
}

#[derive(Debug, Serialize, Deserialize)]

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Set the top-level field to "type": "function" and nest name/description/parameters under a "function" object.
  2. If the source is MCP or Anthropic format, convert through ZeroClaw's tool registry instead of hand-editing JSON.
  3. Validate specs with an equivalent type check before submitting them to chat_with_tools.

Example fix

// before
{"type": "custom", "name": "get_weather", "parameters": {"type": "object"}}

// after
{"type": "function", "function": {"name": "get_weather", "description": "Get weather", "parameters": {"type": "object"}}}
Defensive patterns

Strategy: validation

Validate before calling

// Validate every spec before handing it to chat_with_tools
for tool in &tools {
    let v = serde_json::to_value(tool)?;
    if v.get("type").and_then(|t| t.as_str()) != Some("function") {
        anyhow::bail!("tool {:?} is not type=function", v["function"]["name"]);
    }
}

Type guard

fn is_openai_function_tool(v: &serde_json::Value) -> bool {
    v.get("type").and_then(|t| t.as_str()) == Some("function")
        && v.get("function").and_then(|f| f.get("name")).is_some()
}

Try / catch

Catch the bail from parse_native_tool_spec, include the offending tool's name (from the function object) in your error report, and reject the whole tool batch — a partially converted tool list will fail again on the next call.

Prevention

When it happens

Trigger: Passing tool JSON whose top-level type is not "function" to the OpenAI provider's chat_with_tools path; hand-converting MCP or Anthropic-format tool schemas straight into the OpenAI wire shape; nesting the function body at the top level and omitting the type wrapper.

Common situations: Migrating tool definitions from another provider's schema; hand-written tool JSON where "type" was forgotten or placed inside the "function" object; copying examples that use OpenAI Assistants API tool types.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/30fc22a0f2d571d0. Report an issue: GitHub.