zed-industries/zed · error · ValueError
Claude returned no tool_use block for tool '{tool['name']}'
Error message
Claude returned no tool_use block for tool '{tool['name']}' What it means
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.
Source
Thrown at script/github-check-new-issue-for-duplicates.py:284
Forcing a tool call makes the API emit schema-shaped JSON via its tool-use mechanism
instead of free-form text we'd have to parse out of prose or markdown fences. Raises on
non-2xx status, or if no tool_use block is returned.
"""
data = _claude_request(api_key, {
"max_tokens": max_tokens,
"system": system_prompt,
"messages": [{"role": "user", "content": user_content}],
"tools": [tool],
"tool_choice": {"type": "tool", "name": tool["name"]},
})
if data.get("stop_reason") == "max_tokens":
log(" Warning: response hit max_tokens; structured output may be truncated")
for block in data.get("content", []):
if block.get("type") == "tool_use":
return block.get("input") or {}
raise ValueError(f"Claude returned no tool_use block for tool '{tool['name']}'")
def fetch_issue(issue_number: int):
"""Fetch issue from GitHub and return as a dict."""
log(f"Fetching issue #{issue_number}")
issue_data = github_api_get(f"/repos/{REPO_OWNER}/{REPO_NAME}/issues/{issue_number}")
issue = {
"number": issue_number,
"title": issue_data["title"],
"body": issue_data.get("body") or "",
"author": (issue_data.get("user") or {}).get("login") or "",
"type": (issue_data.get("type") or {}).get("name"),
}
log(f" Title: {issue['title']}\n Type: {issue['type']}\n Author: {issue['author']}")
return issue
View on GitHub (pinned to bc538def45)
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
Example fix
// before
if data.get("stop_reason") == "max_tokens":
log(" Warning: response hit max_tokens; structured output may be truncated")
for block in data.get("content", []):
if block.get("type") == "tool_use":
return block.get("input") or {}
raise ValueError(f"Claude returned no tool_use block for tool '{tool['name']}'")
// after
if data.get("stop_reason") != "tool_use":
preview = next((b.get("text", "") for b in data.get("content", []) if b.get("type") == "text"), "")
raise ValueError(
f"Claude stop_reason={data.get('stop_reason')!r} for tool '{tool['name']}'; text: {preview[:200]}"
)
for block in data.get("content", []):
if block.get("type") == "tool_use":
return block.get("input") or {}
raise ValueError(f"Claude returned no tool_use block for tool '{tool['name']}'") Defensive patterns
Strategy: retry
Validate before calling
def tool_definition_is_forcible(tool: dict) -> bool:
return (
bool(tool.get("name"))
and isinstance(tool.get("input_schema"), dict)
and isinstance(tool["input_schema"].get("properties"), dict)
) Type guard
def has_tool_use_block(data: dict) -> bool:
return any(b.get("type") == "tool_use" for b in data.get("content", [])) Try / catch
for attempt in range(3):
try:
return call_claude_tool(api_key, system_prompt, user_content, tool, max_tokens=4096)
except ValueError:
if attempt == 2:
raise Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Anthropic does not support custom tools
- eval-cli binary not found at {binary}. Build it with: cargo
- No eval-cli binary provided. Either pass binary_path=/path/t
- Could not detect a working directory in the container. Set E
- unknown benchmark '{benchmark_id}' (valid: {valid})
AI-assisted analysis of zed-industries/zed@bc538def45 (2026-08-16).
Data as JSON: /api/errors/c20b02d13b3e5d79.
Report an issue: GitHub.