{"record":{"id":"9d2da06312124d1a","repo":"aaif-goose/goose","slug":"missing-tool-use-id-9d2da0","errorCode":null,"errorMessage":"Missing tool_use id","messagePattern":"Missing tool_use id","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/goose-provider-types/src/formats/snowflake.rs","lineNumber":249,"sourceCode":"            }\n        }\n    };\n\n    // Process all content items in the list\n    for content in content_list {\n        match content.get(\"type\").and_then(|t| t.as_str()) {\n            Some(\"text\") => {\n                if let Some(text) = content.get(\"text\").and_then(|t| t.as_str()) {\n                    if !text.is_empty() {\n                        message = message.with_text(text.to_string());\n                    }\n                }\n            }\n            Some(\"tool_use\") => {\n                let id = content\n                    .get(\"tool_use_id\")\n                    .and_then(|i| i.as_str())\n                    .ok_or_else(|| anyhow!(\"Missing tool_use id\"))?;\n                let name = content\n                    .get(\"name\")\n                    .and_then(|n| n.as_str())\n                    .ok_or_else(|| anyhow!(\"Missing tool_use name\"))?\n                    .to_string();\n\n                let input = content\n                    .get(\"input\")\n                    .ok_or_else(|| anyhow!(\"Missing tool input\"))?\n                    .clone();\n\n                let tool_call = CallToolRequestParams::new(name).with_arguments(object(input));\n                message = message.with_tool_request(id, Ok(tool_call));\n            }\n            Some(\"thinking\") => {\n                let thinking = content\n                    .get(\"thinking\")\n                    .and_then(|t| t.as_str())","sourceCodeStart":231,"sourceCodeEnd":267,"githubUrl":"https://github.com/aaif-goose/goose/blob/3810898a7447ec3299be72e223d3570a7aabf0ab/crates/goose-provider-types/src/formats/snowflake.rs#L231-L267","documentation":"response_to_message converts a Snowflake Cortex (Anthropic-compatible) non-streaming response into a Message. For each entry in content_list with type \"tool_use\" it requires a string field tool_use_id to key the tool_request block. If tool_use_id is missing or not a string, conversion fails with 'Missing tool_use id'.","triggerScenarios":"A Snowflake Cortex response whose content_list contains a {\"type\":\"tool_use\",...} block without a string tool_use_id field. Typical when the API response shape changes, when a proxy rewrites field names (Anthropic native uses \"id\" while this parser expects \"tool_use_id\"), or when the model emits a truncated/malformed tool call.","commonSituations":"Snowflake Cortex API version drift renaming id to tool_use_id or vice versa; testing against recorded fixtures that use the Anthropic wire format (id) instead of Snowflake's content_list format; responses from a Cortex SQL/function wrapper that drops ids.","solutions":["Log the raw response JSON and inspect the tool_use block for the tool_use_id key and its type","Confirm you are hitting the real Snowflake Cortex endpoint whose tool_use blocks carry tool_use_id","Update fixtures/mocks to include \"tool_use_id\" as a string on every tool_use block","If upstream genuinely omits ids, skip the malformed block with a warn (see exampleFix) instead of failing the whole message"],"exampleFix":"// before\nlet id = content\n    .get(\"tool_use_id\")\n    .and_then(|i| i.as_str())\n    .ok_or_else(|| anyhow!(\"Missing tool_use id\"))?;\n\n// after - skip malformed block, keep the rest of the response\nlet id = match content.get(\"tool_use_id\").and_then(|i| i.as_str()) {\n    Some(id) => id,\n    None => {\n        tracing::warn!(\"skipping tool_use block without tool_use_id\");\n        continue;\n    }\n};","handlingStrategy":"validation","validationCode":"// validate content_list before response_to_message\nfor block in content_list {\n    if block.get(\"type\").and_then(|t| t.as_str()) == Some(\"tool_use\")\n        && block.get(\"tool_use_id\").and_then(|i| i.as_str()).is_none()\n    {\n        anyhow::bail!(\"tool_use block without string tool_use_id: {block}\");\n    }\n}","typeGuard":"fn tool_use_block_is_wellformed(block: &serde_json::Value) -> bool {\n    if block.get(\"type\").and_then(|t| t.as_str()) != Some(\"tool_use\") {\n        return true;\n    }\n    block.get(\"tool_use_id\").and_then(|v| v.as_str()).is_some()\n}","tryCatchPattern":"match response_to_message(&response) {\n    Ok(msg) => msg,\n    Err(err) if err.to_string().contains(\"Missing tool_use id\") => {\n        tracing::warn!(\"discarding malformed tool_use: {err}\");\n        Message::assistant()\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Keep Snowflake Cortex fixtures in the exact content_list wire shape (tool_use_id, not Anthropic-native id)","Log raw Cortex responses at debug level in integration environments","Re-verify response shapes after Cortex API version upgrades","When adapting Anthropic-format traffic, write an explicit mapper instead of forwarding blocks unchanged"],"tags":["rust","snowflake","cortex","tool-calls","parsing"],"backgroundTag":null,"analyzedSha":"3810898a7447ec3299be72e223d3570a7aabf0ab","analyzedAt":"2026-08-16T10:14:26.282Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}