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
- Set the top-level field to "type": "function" and nest name/description/parameters under a "function" object.
- If the source is MCP or Anthropic format, convert through ZeroClaw's tool registry instead of hand-editing JSON.
- 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
- Build tool specs through ZeroClaw's tool registry instead of hand-written JSON
- Lint tool JSON in CI with an exact-shape check (type=function + nested function object)
- Never hand-translate MCP or Anthropic tool schemas into OpenAI wire format
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
- Invalid Azure OpenAI tool specification: unsupported tool ty
- Step {step_number} {phase} schema validation failed: {reason
- Schema missing required 'type' field
- ACP request_permission failed: {} ({})
- ACP request_permission timed out after {timeout:?}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/30fc22a0f2d571d0.
Report an issue: GitHub.