Symptoms
A response ends with stop_reason="tool_use" and contains both a client tool_use and a server_tool_use, but the latter has no result yet. A spinner can remain open, or a continuation with extra text can fail with HTTP 400 invalid_request_error. This is not, by itself, proof of permanently lost output.
TypeScript issue #1090 reported this with web fetch. The June 26, 2026 maintainer response identifies an intended flow whose documentation was incomplete. Python issue #1325 described code-execution/tool-search symptoms; its August 6 follow-up reports server-side handling and says synthetic results are unnecessary. Both issues were closed when reviewed. Do not quote the original reports without their follow-ups.
Cause
The mixed-tool contract separates two cases. For a direct parallel call, the server tool waits while the application handles client tools; its result arrives in the next response after valid client results. Correlate across responses by tool_use_id, not adjacency. Preserve the ordered assistant/user/assistant exchange.
In programmatic tool calling, code is already running and pauses for client data. Inspect caller and its tool_id; the code’s final result can require multiple exchanges. This is different from pause_turn, which resumes the saved assistant content without inventing a client-result message. The stop-reason reference defines that distinction.
Safe resolution
For the unfinished mixed turn, return one actual tool_result for every client tool_use in a single immediate user message. Include no extra top-level text, before or after those results. Keep the original tools definitions unchanged. Removing the waiting tool or prematurely ending the turn can cause 400. Add new user instructions only after the turn completes. Do not execute a server call locally or manufacture its result.
The client-result contract allows actual failures through is_error: true. Text nested inside a tool_result is result data, not the forbidden sibling text block. Preserve all assistant blocks, including signed or non-text content, rather than flattening them.
This application example operates on complete JSON-shaped response dictionaries. It builds continuation fields without calling the SDK or executing tools. Take an immutable snapshot of the original tools before the first request; supply actual client outcomes from your separately authorized dispatcher.
from copy import deepcopy
def pending_server_calls(previous, response):
"""Track unmatched IDs across complete responses, not block positions."""
pending = dict(previous)
for block in response["content"]:
if block["type"] in ("server_tool_use", "mcp_tool_use"):
pending[block["id"]] = block["name"]
for block in response["content"]:
if block["type"].endswith("_tool_result"):
pending.pop(block.get("tool_use_id"), None)
return pending # Result observed does NOT mean execution succeeded.
def continuation_patch(history, response, original_tools, results=()):
"""Build request fields offline; history excludes this response.
original_tools is the saved definition snapshot from the initial request.
results contains actual client outcomes, never invented server outcomes.
Keep model, max_tokens and other applicable request settings separately.
"""
calls = [b for b in response["content"] if b["type"] == "tool_use"]
reason = response["stop_reason"]
if reason == "pause_turn":
if calls or results:
raise ValueError("pause_turn is not a client-result continuation")
elif reason == "tool_use":
if not calls or any(r.get("type") != "tool_result" for r in results):
raise ValueError("Return client tool_result blocks only")
expected = {b["id"] for b in calls}
actual = [r.get("tool_use_id") for r in results]
if len(actual) != len(expected) or set(actual) != expected:
raise ValueError("Return exactly one result per client tool_use")
by_id = {b["id"]: b for b in calls}
for result in results:
if result.get("toolset_name") != by_id[result["tool_use_id"]].get("toolset_name"):
raise ValueError("Preserve the client toolset_name")
else:
raise ValueError("Handle completion, refusal or truncation separately")
container_id = (response.get("container") or {}).get("id")
programmatic = any(
(b.get("caller") or {}).get("type", "direct").startswith("code_execution_")
for b in calls
)
if programmatic and not container_id:
raise ValueError("Programmatic continuation requires container.id")
messages = deepcopy(history)
messages.append({"role": "assistant", "content": deepcopy(response["content"])})
if reason == "tool_use":
messages.append({"role": "user", "content": deepcopy(list(results))})
patch = {"messages": messages, "tools": deepcopy(original_tools)}
if container_id:
patch["container"] = container_id
return patch
For programmatic calls, reuse the returned container.id; never replace it with a new container. Pass the patch with the same applicable model/request settings. Preserve the next assistant response in history before another exchange. A pending-ID map is observation state, not a success ledger: examine result content for server errors. The pinned SDK result type exposes tool_use_id and web_fetch_tool_result, but types alone do not prove execution.
Use a bounded continuation budget and a visible unresolved state. If a terminal response still leaves an unmatched ID, retain diagnostic evidence and investigate; do not silently mark it successful or retry a side-effecting client operation blindly.
Verification
Reviewed official documentation, pinned SDK types and issue follow-ups on 2026-10-23. No Anthropic API request was made; upstream observations are attributed reports, not a live reproduction by API Atlas.
The repository’s offline controls execute this published example with synthetic dictionaries. Normal controls cover multiple client results, out-of-order ID correlation, a server result already present, delayed results across responses, client-error-shaped results (is_error: true), pause_turn, and multi-round programmatic continuation. Rejection controls cover extra top-level text, missing/duplicate/wrong client IDs, fabricated server results, missing programmatic containers, and attempts to resume terminal or truncated responses. Definition/history preservation is checked without a network request.
In your own authorized live test, record the model, API/SDK/tool versions, stop reasons, IDs, caller and container metadata, and sanitized request/response sequence. Compare the valid result-only continuation with extra-text and removed-tool failure controls. These server-side 400 controls were not run here. Static schema, evidence hashes, translation and build checks cannot establish service availability.
Limitations
The Python report’s SDK 0.52.0, claude-sonnet-4-6 and code_execution_20250825 identify that issue’s environment, not a current recommended combination. No first-affected version, universal SDK fix version or model-wide failure rate was established. Recheck the current tool/model compatibility documentation for the endpoint you use; direct Anthropic, Vertex and Bedrock support must not be assumed identical.
The example is not an SDK tool runner, streaming assembler or automatic retry system. It does not validate every payload field, enforce container expiry, authorize client actions, or establish whether a pending operation succeeded. Store diagnostics under your retention policy and redact secrets. No GetBeta production fix is claimed or implemented; a text-only adapter path does not demonstrate mixed-tool handling.
Use the Anthropic provider profile and add these cases to the LLM API operational-fit evaluation. Search phrases are editorial candidates, not measured search volume, rankings or evidence of adoption.
Sources and verification scope
Reviewed official documentation, pinned SDK types and issue follow-ups on 2026-10-23. No Anthropic API request was made; upstream observations are attributed reports, not a live reproduction by API Atlas.
- anthropic-mixed-contract (documentation) · checked:
- anthropic-stop-reasons (documentation) · checked:
- anthropic-client-results (documentation) · checked:
- anthropic-programmatic (documentation) · checked:
- anthropic-web-fetch-result-type (code) · checked:
- anthropic-mixed-ts-issue (documentation) · checked:
- anthropic-mixed-python-issue (documentation) · checked:
- anthropic-mixed-python-followup (documentation) · checked: