{"record":{"id":"30fc22a0f2d571d0","repo":"zeroclaw-labs/zeroclaw","slug":"invalid-openai-tool-specification-unsupported-too","errorCode":null,"errorMessage":"Invalid OpenAI tool specification: unsupported tool type '{}', expected 'function'","messagePattern":"Invalid OpenAI tool specification: unsupported tool type '(.+?)', expected 'function'","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-providers/src/openai.rs","lineNumber":160,"sourceCode":"    /// built specs.\n    #[serde(flatten)]\n    pub(crate) extra: serde_json::Map<String, serde_json::Value>,\n}\n\npub(crate) fn parse_native_tool_spec(value: serde_json::Value) -> anyhow::Result<NativeToolSpec> {\n    let spec: NativeToolSpec = serde_json::from_value(value).map_err(|e| {\n        ::zeroclaw_log::record!(\n            WARN,\n            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)\n                .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                .with_attrs(::serde_json::json!({\"error\": format!(\"{}\", e)})),\n            \"openai: invalid tool spec\"\n        );\n        anyhow::Error::msg(format!(\"Invalid OpenAI tool specification: {e}\"))\n    })?;\n\n    if spec.kind != \"function\" {\n        anyhow::bail!(\n            \"Invalid OpenAI tool specification: unsupported tool type '{}', expected 'function'\",\n            spec.kind\n        );\n    }\n\n    Ok(spec)\n}\n\n#[derive(Debug, Serialize, Deserialize)]\nstruct NativeToolCall {\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    id: Option<String>,\n    #[serde(rename = \"type\", skip_serializing_if = \"Option::is_none\")]\n    kind: Option<String>,\n    function: NativeFunctionCall,\n}\n\n#[derive(Debug, Serialize, Deserialize)]","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-providers/src/openai.rs#L142-L178","documentation":"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\"}}.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n{\"type\": \"custom\", \"name\": \"get_weather\", \"parameters\": {\"type\": \"object\"}}\n\n// after\n{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get weather\", \"parameters\": {\"type\": \"object\"}}}","handlingStrategy":"validation","validationCode":"// Validate every spec before handing it to chat_with_tools\nfor tool in &tools {\n    let v = serde_json::to_value(tool)?;\n    if v.get(\"type\").and_then(|t| t.as_str()) != Some(\"function\") {\n        anyhow::bail!(\"tool {:?} is not type=function\", v[\"function\"][\"name\"]);\n    }\n}","typeGuard":"fn is_openai_function_tool(v: &serde_json::Value) -> bool {\n    v.get(\"type\").and_then(|t| t.as_str()) == Some(\"function\")\n        && v.get(\"function\").and_then(|f| f.get(\"name\")).is_some()\n}","tryCatchPattern":"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.","preventionTips":["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"],"tags":["openai","tools","validation","llm-provider","json-schema"],"backgroundTag":"invalid-tool-definition","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}