{"record":{"id":"c20b02d13b3e5d79","repo":"zed-industries/zed","slug":"claude-returned-no-tool-use-block-for-tool-tool","errorCode":null,"errorMessage":"Claude returned no tool_use block for tool '{tool['name']}'","messagePattern":"Claude returned no tool_use block for tool '(.+?)'","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"script/github-check-new-issue-for-duplicates.py","lineNumber":284,"sourceCode":"    Forcing a tool call makes the API emit schema-shaped JSON via its tool-use mechanism\n    instead of free-form text we'd have to parse out of prose or markdown fences. Raises on\n    non-2xx status, or if no tool_use block is returned.\n    \"\"\"\n    data = _claude_request(api_key, {\n        \"max_tokens\": max_tokens,\n        \"system\": system_prompt,\n        \"messages\": [{\"role\": \"user\", \"content\": user_content}],\n        \"tools\": [tool],\n        \"tool_choice\": {\"type\": \"tool\", \"name\": tool[\"name\"]},\n    })\n\n    if data.get(\"stop_reason\") == \"max_tokens\":\n        log(\"  Warning: response hit max_tokens; structured output may be truncated\")\n\n    for block in data.get(\"content\", []):\n        if block.get(\"type\") == \"tool_use\":\n            return block.get(\"input\") or {}\n    raise ValueError(f\"Claude returned no tool_use block for tool '{tool['name']}'\")\n\n\ndef fetch_issue(issue_number: int):\n    \"\"\"Fetch issue from GitHub and return as a dict.\"\"\"\n    log(f\"Fetching issue #{issue_number}\")\n\n    issue_data = github_api_get(f\"/repos/{REPO_OWNER}/{REPO_NAME}/issues/{issue_number}\")\n    issue = {\n        \"number\": issue_number,\n        \"title\": issue_data[\"title\"],\n        \"body\": issue_data.get(\"body\") or \"\",\n        \"author\": (issue_data.get(\"user\") or {}).get(\"login\") or \"\",\n        \"type\": (issue_data.get(\"type\") or {}).get(\"name\"),\n    }\n\n    log(f\"  Title: {issue['title']}\\n  Type: {issue['type']}\\n  Author: {issue['author']}\")\n    return issue\n","sourceCodeStart":266,"sourceCodeEnd":302,"githubUrl":"https://github.com/zed-industries/zed/blob/bc538def4545534201bbfcac4e95ac34ea6501b6/script/github-check-new-issue-for-duplicates.py#L266-L302","documentation":"call_claude_tool forces a specific tool via tool_choice {\"type\": \"tool\"} and expects the Anthropic Messages API response to contain a tool_use content block whose input is the structured payload. If the response holds only text blocks — because the model hit max_tokens before emitting the call (default budget 1024), refused, or returned an unusual stop_reason — the scan loop finds nothing and raises this ValueError. The code merely logs a warning for stop_reason == \"max_tokens\" and then raises anyway.","triggerScenarios":"POST https://api.anthropic.com/v1/messages with tools + forced tool_choice returns stop_reason \"max_tokens\" with a truncated text block and no tool_use; stop_reason \"refusal\" or \"pause_turn\" due to content filtering; or a model/anthropic-version change that makes the model answer in text despite forced tool_choice.","commonSituations":"Large issue bodies fed as user_content pushing the tool call past the 1024-token budget; verbose tool input schemas; switching CLAUDE_MODEL to one with different tool-use behavior; occasionally nondeterministic truncation even at temperature 0.0.","solutions":["Raise max_tokens for the structured call (2048-4096) so the forced tool call always fits","Retry the request once or twice: truncation and refusal are often nondeterministic even with temperature 0.0","Check stop_reason before scanning blocks and fail with the text-block preview when it is not \"tool_use\"","Verify the name in tool_choice exactly matches the tool definition's name field"],"exampleFix":"// before\nif data.get(\"stop_reason\") == \"max_tokens\":\n    log(\"  Warning: response hit max_tokens; structured output may be truncated\")\nfor block in data.get(\"content\", []):\n    if block.get(\"type\") == \"tool_use\":\n        return block.get(\"input\") or {}\nraise ValueError(f\"Claude returned no tool_use block for tool '{tool['name']}'\")\n\n// after\nif data.get(\"stop_reason\") != \"tool_use\":\n    preview = next((b.get(\"text\", \"\") for b in data.get(\"content\", []) if b.get(\"type\") == \"text\"), \"\")\n    raise ValueError(\n        f\"Claude stop_reason={data.get('stop_reason')!r} for tool '{tool['name']}'; text: {preview[:200]}\"\n    )\nfor block in data.get(\"content\", []):\n    if block.get(\"type\") == \"tool_use\":\n        return block.get(\"input\") or {}\nraise ValueError(f\"Claude returned no tool_use block for tool '{tool['name']}'\")","handlingStrategy":"retry","validationCode":"def tool_definition_is_forcible(tool: dict) -> bool:\n    return (\n        bool(tool.get(\"name\"))\n        and isinstance(tool.get(\"input_schema\"), dict)\n        and isinstance(tool[\"input_schema\"].get(\"properties\"), dict)\n    )","typeGuard":"def has_tool_use_block(data: dict) -> bool:\n    return any(b.get(\"type\") == \"tool_use\" for b in data.get(\"content\", []))","tryCatchPattern":"for attempt in range(3):\n    try:\n        return call_claude_tool(api_key, system_prompt, user_content, tool, max_tokens=4096)\n    except ValueError:\n        if attempt == 2:\n            raise","preventionTips":["Budget max_tokens generously for forced tool calls","Always inspect stop_reason before scanning content blocks","Trim large issue bodies before sending them as user content","Re-test tool calls whenever CLAUDE_MODEL or the API version changes"],"tags":["anthropic","claude","llm","tool-use","structured-output","python"],"backgroundTag":null,"analyzedSha":"bc538def4545534201bbfcac4e95ac34ea6501b6","analyzedAt":"2026-08-16T07:30:46.435Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}