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

  1. Raise max_tokens for the structured call (2048-4096) so the forced tool call always fits
  2. Retry the request once or twice: truncation and refusal are often nondeterministic even with temperature 0.0
  3. Check stop_reason before scanning blocks and fail with the text-block preview when it is not "tool_use"
  4. 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

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


AI-assisted analysis of zed-industries/zed@bc538def45 (2026-08-16). Data as JSON: /api/errors/c20b02d13b3e5d79. Report an issue: GitHub.