zeroclaw-labs/zeroclaw · error · anyhow::Error
Invalid Azure OpenAI tool specification: unsupported tool ty
Error message
Invalid Azure OpenAI tool specification: unsupported tool type '{}', expected 'function' What it means
parse_native_tool_spec validates each tool definition serialized for the Azure OpenAI chat completions API. Azure OpenAI implements only the 'function' tool type, so a spec whose JSON type field is anything else ('custom', 'code_interpreter', 'retrieval', or a typo) is rejected locally before any HTTP request is sent.
Source
Thrown at crates/zeroclaw-providers/src/azure_openai.rs:135
/// `Arc`-shared with the tool registry's stored schema — serialized
/// transparently, never deep-cloned per request
parameters: std::sync::Arc<serde_json::Value>,
}
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)})),
"azure_openai: invalid tool spec"
);
anyhow::Error::msg(format!("Invalid Azure OpenAI tool specification: {e}"))
})?;
if spec.kind != "function" {
anyhow::bail!(
"Invalid Azure 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 every tool definition's type field to "function" with a nested function object carrying name, description, and parameters
- Strip or convert non-function tools (custom, code_interpreter, retrieval) before passing them to an azure provider
- If you need non-function tool types, route those requests to a provider family that supports them instead of azure_openai
Example fix
// before
let tool = serde_json::json!({
"type": "custom",
"name": "get_weather",
"parameters": {"city": {"type": "string"}}
});
// after
let tool = serde_json::json!({
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
}
}); Defensive patterns
Strategy: validation
Validate before calling
// Filter/normalize tools before handing them to an azure provider
let tools: Vec<serde_json::Value> = raw_tools
.into_iter()
.filter(|t| t.get("type").and_then(|v| v.as_str()) == Some("function"))
.collect();
if tools.is_empty() {
anyhow::bail!("no function-type tools to send");
} Type guard
fn is_function_tool_spec(value: &serde_json::Value) -> bool {
value.get("type").and_then(|t| t.as_str()) == Some("function")
&& value.get("function").is_some_and(|f| {
f.get("name").is_some_and(|n| n.as_str().is_some_and(|s| !s.is_empty()))
})
} Try / catch
match provider.chat_with_tools(/* ... */).await {
Ok(resp) => { /* ... */ }
Err(e) if e.to_string().contains("unsupported tool type") => {
// re-validate the tool list, convert or drop non-function tools, retry once
}
Err(e) => return Err(e),
} Prevention
- Emit tool definitions from one place with type: "function" fixed
- Validate the tool list with the type guard before every azure chat-with-tools call
- Do not port tool JSON between provider formats without mapping the type field
- Add a unit test asserting every registered tool serializes with type == "function"
When it happens
Trigger: Calling chat with native tools on an azure family provider when a tool definition's {"type": ...} is not the exact string "function" - for example a tool ported from the OpenAI Responses API ('custom' tools) or Anthropic format, or hand-written JSON with a typo.
Common situations: Porting tool definitions between provider SDKs; a tool registry that emits a default or empty type field; JSON tool schemas authored against newer OpenAI tool types.
Related errors
- WhatsApp interactive buttons require 1..=3 options (got {});
- WhatsApp interactive list requires at least one section
- WhatsApp interactive list capped at 10 sections (got {})
- WhatsApp interactive list section '{}' capped at 10 rows (go
- WhatsApp location marker must be `lat,lng[,name[,address]]`
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/8b9fc928fb51c944.
Report an issue: GitHub.