Anthropic retired the direct Claude API ID claude-sonnet-4-20250514 on June 15, 2026 and recommends Sonnet 5.5 as its replacement. An application still naming that ID needs more than a new model string: the cumulative Sonnet 5.5 migration guide covers changed request fields, thinking, tool choice and response handling. This procedure is for the owner of one existing nonstreaming, text-only, direct Messages API application path. It takes that whole path, including its actual client tools and JSON output when used, to an evidence-backed release decision or a precise HOLD.
The big change
- What changed: Sonnet 4's Claude API ID is retired, while the recommended Sonnet 5.5 path has a different request and response contract. A model-name edit leaves old samplers, thinking budgets, forced tool selection or parser assumptions in place.
- Why it matters: An application can receive a successful HTTP response yet fail its real task or mishandle a tool that changes another system. The owner needs to check the complete request, all model turns, the resulting effects and the billable usage against cases approved for that application.
- What to watch: The release decision depends on the same exact code, prompt, request and API-version revision that passed those cases, followed by observed cost and latency. The retired ID cannot serve as a fresh rollback or a new comparison baseline.
This is a documented integration recipe. BIG CHANGE did not have an owner's application, account, fixtures or bill and did not send a provider request. The reference below compiles and has had local synthetic checks; those checks establish neither Sonnet 5.5 behavior in your account nor a successful migration. Our Sonnet 5.5 release report covers the model announcement. This guide addresses the application repair.
1. Establish the path and release authority
Confirm that the deployed route actually serializes claude-sonnet-4-20250514 to Anthropic's direct POST /v1/messages. Save a redacted copy of its serialized request and HTTP headers, the code and prompt revision, the effective anthropic-version, any resolved SDK version, parser, validators, tool declarations and handlers. A source-code search alone does not establish which runtime route serves traffic. Keep credentials and customer material in your private system.
You need staging access, authority to use Sonnet 5.5 in the actual account, permission for the intended data and test spend, and an owner empowered to release the exact revision. Record whether genuine historical Sonnet 4 response and billing observations exist. Since the Claude API ID is retired, write baseline_unavailable when they do not; do not invent a before-and-after comparison. Anthropic's deprecation record distinguishes Claude API retirement from partner-platform schedules. Amazon Bedrock or Google Cloud model IDs and lifecycle require their own checks and are outside this direct route.
List every feature of this one real call path. The reference covers nonstreaming text, application-defined client tools and JSON output with a real supported schema. A path using streamed or partial tool input, images, documents, computer use, server tools or a partner provider needs its own complete adapter and remains HOLD here. Do not remove a used feature merely to make the path fit.
2. Freeze cases before making a paid call
For each approved case, save the actual input and its provenance, permitted data rights, expected answer and output schema, required and prohibited tool effects, maximum turns, and usage, cost and latency limits. Include a normal task, a denied or negative task, upstream empty/error behavior and an output or context boundary. Add actual client-tool and JSON cases when the application uses them. The owner must approve handlers, effect stubs and any staging write. Neither a generic prompt nor a mock example proves the application's behavior.
Bind each case to the old serialized request hash, candidate request hash, code and prompt revisions, effective API version, model and effort. Save the old request bytes unchanged. Keep a private durable journal and a read-only method for reconciling earlier HTTP attempts or tool effects. An unknown write outcome must be checked against the authoritative external record before another attempt; the reference does not retry automatically.
The private tool_policy supplied to the code needs authorized: true, a nonempty case_id, unique allowed_tool_names and required_tool_names taken from the captured declarations, integer min_tool_calls and max_tool_calls, boolean allow_parallel, and approve_forced_to_auto: true only when the owner explicitly accepts that conversion. Preserve tool_choice: none and compatible auto behavior, including a parallel-use restriction. Forced any or a named tool becomes auto only with that frozen authority. Because auto can choose no tool, a case requiring one must fail when no call occurs.
3. Review the candidate request
Save the following complete Python 3 standard-library module as direct_messages_path.py in the authorized application and review its integration. Importing it sends no request. build_request copies the captured request, changes the model to claude-sonnet-5-5, removes temperature, top_p and top_k even when serialized at defaults, moves a used output_format to output_config.format, and replaces a manual thinking budget with an explicit mode and effort. Its default is adaptive after an old enabled budget and between_tools after old disabled or omitted thinking. That choice is a candidate for owner evaluation, not an equivalent numeric budget. between_tools accepts low, medium or high effort. Recount tokens and set a real bounded max_tokens.
A final assistant prefill needs an owner-reviewed replacement in the output format or system/user instruction; the module holds rather than guessing. It requires strict-compatible JSON schemas for custom client tools and keeps signed thinking blocks in ordered assistant history when continuing a tool call. Inspect the actual HTTP header layer for old beta headers, including interleaved-thinking-2025-05-14, token-efficient-tools-2025-02-19, output-128k-2025-02-19 and any context-window beta. Fine-grained tool streaming belongs to the separate streaming route. If the application uses built-in text editing or code execution, review the current tool versions in Anthropic's migration checklist; this reference holds those paths.
"""Reader reference for one owned, nonstreaming direct Messages call path.
Documentation-only. BIG CHANGE did not send a request or run a reader case.
Supply real, approved messages, case rules, client-tool handler and output validator.
This module deliberately makes one HTTP attempt per turn and never retries a write.
"""
import copy
import base64
import hashlib
import json
import urllib.error
import urllib.request
MODEL = "claude-sonnet-5-5"
ENDPOINT = "https://api.anthropic.com/v1/messages"
class Hold(Exception):
"""A migration case needs an owner decision or a corrected revision."""
class TransportHold(Hold):
def __init__(self, reason, metadata):
super().__init__(reason)
self.metadata = metadata
def _raw_record(raw):
return {"raw_body_base64": base64.b64encode(raw[:65536]).decode("ascii"),
"raw_body_sha256": hashlib.sha256(raw).hexdigest(),
"raw_body_truncated": len(raw) > 65536}
def _case_authority(old_request, case_policy):
if not isinstance(case_policy, dict) or case_policy.get("authorized") is not True or not case_policy.get("case_id"):
raise Hold("Frozen owner case authority and ID required")
tools = old_request.get("tools", [])
if not isinstance(tools, list) or any(not isinstance(t, dict) or not t.get("name") for t in tools):
raise Hold("Malformed captured tool declarations")
names = [t["name"] for t in tools]
if len(set(names)) != len(names):
raise Hold("Duplicate declared tool names")
allowed = case_policy.get("allowed_tool_names")
required = case_policy.get("required_tool_names")
if not isinstance(allowed, list) or not isinstance(required, list) or len(set(allowed)) != len(allowed) or len(set(required)) != len(required):
raise Hold("Freeze unique allowed/required tool names")
allowed, required = set(allowed), set(required)
if not allowed <= set(names) or not required <= allowed:
raise Hold("Case authority exceeds captured declarations")
minimum, maximum = case_policy.get("min_tool_calls"), case_policy.get("max_tool_calls")
if type(minimum) is not int or type(maximum) is not int or minimum < 0 or maximum < minimum:
raise Hold("Freeze bounded integer tool call count")
if type(case_policy.get("allow_parallel")) is not bool:
raise Hold("Freeze parallel-call authority")
choice = old_request.get("tool_choice", {"type": "auto"} if names else {"type": "none"})
if not isinstance(choice, dict) or choice.get("type") not in ("none", "auto", "any", "tool"):
raise Hold("Unknown captured tool choice")
kind = choice["type"]
if kind == "none" and (allowed or required or minimum or maximum):
raise Hold("Captured none forbids tool calls")
if kind == "tool" and (choice.get("name") not in names or allowed != {choice["name"]} or choice["name"] not in required or minimum < 1):
raise Hold("Specific-tool case must require exact captured name")
if kind == "any" and (not required or minimum < 1):
raise Hold("Forced-any completion needs explicit required name(s)")
if kind in ("any", "tool") and case_policy.get("approve_forced_to_auto") is not True:
raise Hold("Owner must approve forced-choice completion change")
if choice.get("disable_parallel_tool_use") is True and case_policy["allow_parallel"]:
raise Hold("Cannot widen captured parallel restriction")
return choice, allowed, required, minimum, maximum
def _object_schema_is_strict(schema):
if not isinstance(schema, dict):
return False
if schema.get("type") == "object" and schema.get("additionalProperties") is not False:
return False
children = list(schema.get("properties", {}).values())
if "items" in schema:
children.append(schema["items"])
for key in ("allOf", "anyOf", "oneOf"):
children.extend(schema.get(key, []))
return all(_object_schema_is_strict(v) for v in children)
def build_request(old_request, approved_messages, *, case_policy, effort="medium", mode=None):
"""Return a separate candidate request and a record of deliberate field edits.
The caller first captures the old serialized request and effective API version.
A real application's remaining request fields, cases and output contract survive.
Unsupported branches hold instead of silently changing application behavior.
"""
choice, _, _, _, _ = _case_authority(old_request, case_policy)
if old_request.get("model") != "claude-sonnet-4-20250514":
raise Hold("Expected the actual retired Sonnet 4 direct API call path")
if old_request.get("stream") or old_request.get("streaming"):
raise Hold("Streaming path requires its own terminal-event and partial-input adapter")
if not approved_messages or approved_messages[-1].get("role") != "user":
raise Hold("Replace a last-assistant prefill under owner review; end with a user turn")
for message in approved_messages:
content = message.get("content")
blocks = content if isinstance(content, list) else []
if any(isinstance(b, dict) and b.get("type") in ("image", "document") for b in blocks):
raise Hold("Image/document path needs separate token and parser review")
request = copy.deepcopy(old_request)
changes = []
request["model"] = MODEL
request["messages"] = copy.deepcopy(approved_messages)
request.pop("stream", None)
for key in ("temperature", "top_p", "top_k"):
if key in request:
del request[key]
changes.append(f"removed {key}, including any default-valued serialization")
if "output_format" in request:
old_format = request.pop("output_format")
request.setdefault("output_config", {})["format"] = old_format
changes.append("moved output_format to output_config.format; validate real schema")
previous_thinking = request.get("thinking", {})
if mode is None:
mode = "adaptive" if previous_thinking.get("type") == "enabled" else "between_tools"
if mode not in ("adaptive", "between_tools") or effort not in ("low", "medium", "high", "xhigh", "max"):
raise Hold("Choose a documented thinking mode and effort from owner cases")
if mode == "between_tools" and effort not in ("low", "medium", "high"):
raise Hold("between_tools is supported at low, medium or high effort")
request["thinking"] = {"type": mode}
request.setdefault("output_config", {})["effort"] = effort
changes.append("replaced disabled/omitted or enabled-budget thinking with explicit mode and effort")
for tool in request.get("tools", []):
if "type" in tool or "input_schema" not in tool:
raise Hold("Server/built-in or unknown tool requires its exact current toolset route")
if not _object_schema_is_strict(tool["input_schema"]):
raise Hold("Client tool schema needs supported strict JSON Schema and additionalProperties:false")
tool["strict"] = True
if choice["type"] in ("any", "tool"):
request["tool_choice"] = {**choice, "type": "auto"}
request["tool_choice"].pop("name", None)
changes.append("owner-approved forced choice to auto with frozen tool case authority")
elif "tool_choice" in old_request:
request["tool_choice"] = copy.deepcopy(choice)
for key in ("anthropic_beta", "betas"):
if key in request:
raise Hold("Review and remove legacy beta headers in the actual HTTP header layer")
if type(request.get("max_tokens")) is not int or request["max_tokens"] <= 0:
raise Hold("Owner must set an actual bounded max_tokens after token recount")
return request, changes
def direct_http_once(request, *, api_key, api_version, timeout=60):
"""One paid HTTP attempt; caller supplies a secret from private storage."""
if not api_key or not api_version:
raise Hold("Missing authorized API key or recorded effective anthropic-version")
body = json.dumps(request, separators=(",", ":")).encode("utf-8")
call = urllib.request.Request(
ENDPOINT,
data=body,
headers={"x-api-key": api_key, "anthropic-version": api_version, "content-type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(call, timeout=timeout) as result:
raw = result.read()
metadata = {"status": result.status, "request_id": result.headers.get("request-id") or result.headers.get("x-request-id"), **_raw_record(raw)}
if result.status != 200:
raise TransportHold("Messages non-200 response", metadata)
try:
message = json.loads(raw)
return {"message": message, "http": {"status": result.status,
"request_id": metadata["request_id"],
"raw_body_sha256": metadata["raw_body_sha256"]}}
except (UnicodeError, json.JSONDecodeError) as exc:
raise TransportHold("Messages invalid JSON", metadata) from exc
except urllib.error.HTTPError as exc:
raw = exc.read()
raise TransportHold("Messages HTTP error", {"status": exc.code, "request_id": exc.headers.get("request-id") or exc.headers.get("x-request-id"), **_raw_record(raw)}) from exc
except (urllib.error.URLError, TimeoutError) as exc:
raise TransportHold("Transport outcome unknown", {"status": None, "request_id": None, "error_type": type(exc).__name__}) from exc
def _check_usage(usage):
if not isinstance(usage, dict) or any(k not in usage for k in ("input_tokens", "output_tokens")):
raise Hold("Mandatory raw usage absent")
for key, value in usage.items():
if key.endswith("_tokens") and (type(value) is not int or value < 0):
raise Hold(f"Invalid raw usage counter: {key}")
if key == "cache_creation" and isinstance(value, dict):
for subkey, subvalue in value.items():
if subkey.endswith("_tokens") and (type(subvalue) is not int or subvalue < 0):
raise Hold(f"Invalid raw usage counter: {subkey}")
def run_case(request, *, case_policy, identity, reconcile_prior, transport, execute_tool, validate_result, journal, max_turns=4):
"""Inspect a complete owner case; callbacks belong to the real application.
transport(request) returns {message: parsed Messages JSON, http: status,
request_id and raw_body_sha256}. A custom parsed-JSON-only transport is accepted
but its HTTP status and request ID remain unknown. execute_tool(case_id, tool_use_id, name, input)
returns a dict with content, effect_state (none/confirmed), and an owner receipt.
validate_result(text, effects) is the owner's frozen schema/task/effect oracle.
journal(event) durably commits private attempts, responses, intents and receipts.
reconcile_prior(binding) returns True only when no earlier attempt or intent is
unresolved after authoritative readback. It must not assume idempotency.
Unknown effects, absent usage or incomplete turns hold. No automatic retry.
"""
_, allowed, required, minimum, maximum = _case_authority(request, case_policy)
if not isinstance(identity, dict) or identity.get("case_id") != case_policy["case_id"] or not all(identity.get(k) for k in ("code_revision", "original_request_sha256", "api_version")):
raise Hold("Bind case to exact original request, code revision and API version")
current = copy.deepcopy(request)
observations = []
effects = []
called_names = []
seen_tool_ids = set()
if journal is None or type(max_turns) is not int or max_turns < 1:
raise Hold("Provide a durable private case journal and positive turn bound")
binding = {**identity, "candidate_request_sha256": hashlib.sha256(json.dumps(request, sort_keys=True, separators=(",", ":")).encode()).hexdigest(), "model": request.get("model")}
def emit(kind, **fields):
try:
journal({"event": kind, "binding": copy.deepcopy(binding), **fields})
except Exception as exc:
raise Hold("Durable journal failed; effect/attempt state uncertain; authoritative readback required") from exc
try:
try:
reconciled = reconcile_prior is not None and reconcile_prior(copy.deepcopy(binding)) is True
except Exception as exc:
raise Hold("Prior-intent reconciliation failed; authoritative readback required") from exc
if not reconciled:
raise Hold("Earlier attempt/intent unresolved; authoritative readback before rerun")
for turn in range(1, max_turns + 1):
request_hash = hashlib.sha256(json.dumps(current, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
emit("request_attempt", turn=turn, request_sha256=request_hash, request_raw=copy.deepcopy(current))
try:
transported = transport(current)
except Exception as exc:
emit("request_error", turn=turn, request_sha256=request_hash, reason=str(exc), metadata=getattr(exc, "metadata", {"error_type": type(exc).__name__}))
raise Hold("Transport outcome/error requires exact-case reconciliation") from exc
if isinstance(transported, dict) and "message" in transported and "http" in transported:
response = transported["message"]
http = copy.deepcopy(transported["http"])
invalid_http = not isinstance(http, dict) or type(http.get("status")) is not int or http["status"] != 200
else:
response = transported
http = {"status": None, "request_id": None, "raw_body_sha256": None}
invalid_http = False
entry = {
"turn": turn,
"http": copy.deepcopy(http),
"response_id": response.get("id") if isinstance(response, dict) else None,
"stop_reason": response.get("stop_reason") if isinstance(response, dict) else None,
"usage_raw": copy.deepcopy(response.get("usage")) if isinstance(response, dict) else None,
"response_raw": copy.deepcopy(response),
}
emit("response", turn=turn, request_sha256=request_hash, http=copy.deepcopy(http),
response_id=entry["response_id"], response_raw=copy.deepcopy(response))
observations.append(entry)
if invalid_http:
raise Hold("Successful transport envelope needs observed HTTP 200 metadata")
if not isinstance(response, dict):
raise Hold("Messages response is not a JSON object")
if response.get("model") != MODEL:
raise Hold("Returned model differs from the candidate ID")
usage = response.get("usage")
_check_usage(usage)
content = response.get("content")
if not isinstance(content, list):
raise Hold("Missing typed content blocks")
text = []
calls = []
for block in content:
if not isinstance(block, dict):
raise Hold("Malformed typed content block")
kind = block.get("type")
if kind == "text":
if not isinstance(block.get("text"), str):
raise Hold("Malformed text block")
text.append(block.get("text", ""))
elif kind == "tool_use":
if not block.get("id") or not block.get("name"):
raise Hold("Malformed client tool call")
calls.append(block)
elif kind in ("thinking", "redacted_thinking"):
pass # Preserve complete signed block in assistant history below.
else:
raise Hold(f"Unexpected content block type: {kind}")
stop = response.get("stop_reason")
if stop == "tool_use":
if not calls or execute_tool is None:
raise Hold("Required client tool continuation unavailable")
ids = [b.get("id") for b in calls]
names = [b.get("name") for b in calls]
if any(not isinstance(i, str) or not i for i in ids) or len(ids) != len(set(ids)) or set(ids) & seen_tool_ids or any(n not in allowed for n in names):
raise Hold("Duplicate/malformed tool IDs or undeclared/unauthorized tool names")
if not case_policy["allow_parallel"] and len(calls) > 1:
raise Hold("Parallel tool calls exceed frozen case authority")
if len(called_names) + len(calls) > maximum:
raise Hold("Tool call count exceeds frozen case authority")
parsed = []
for block in calls:
value = block.get("input")
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError as exc:
raise Hold("Client tool input is invalid JSON") from exc
if not isinstance(value, dict):
raise Hold("Client tool input is not a JSON object")
parsed.append(value)
current["messages"].append({"role": "assistant", "content": copy.deepcopy(content)})
results = []
for block, value in zip(calls, parsed):
emit("tool_intent", turn=turn, tool_use_id=block["id"], name=block["name"], input=copy.deepcopy(value))
try:
outcome = execute_tool(identity["case_id"], block["id"], block["name"], value)
except Exception as exc:
emit("tool_unknown", turn=turn, tool_use_id=block["id"], name=block["name"], error_type=type(exc).__name__)
raise Hold("Tool effect unknown; reconcile exact call by readback before any retry") from exc
if not isinstance(outcome, dict):
emit("tool_unknown", turn=turn, tool_use_id=block["id"], name=block["name"], reason="missing receipt object")
raise Hold("Tool effect unknown; handler supplied no receipt object")
if outcome.get("effect_state") not in ("none", "confirmed") or not outcome.get("receipt") or "content" not in outcome:
emit("tool_unknown", turn=turn, tool_use_id=block["id"], name=block["name"], reason="uncertain receipt")
raise Hold("Tool effect uncertain; reconcile by tool readback before any retry")
emit("tool_receipt", turn=turn, tool_use_id=block["id"], name=block["name"], effect_state=outcome["effect_state"], receipt=copy.deepcopy(outcome["receipt"]))
effects.append(outcome["receipt"])
seen_tool_ids.add(block["id"])
results.append({"type": "tool_result", "tool_use_id": block["id"], "content": outcome["content"]})
called_names.append(block["name"])
current["messages"].append({"role": "user", "content": results})
continue
if stop != "end_turn":
raise Hold(f"Nonterminal or failed stop_reason={stop}; inspect and repair before rerun")
if calls or len(called_names) < minimum or not required <= set(called_names):
raise Hold("A required tool was not completed; auto tool choice can make no call")
answer = "".join(text)
if not answer.strip() or validate_result is None:
raise Hold("Empty answer or missing owner result validator")
try:
accepted = validate_result(answer, effects)
except Exception as exc:
raise Hold("Owner result/schema validator failed") from exc
if accepted is not True:
raise Hold("Empty, schema-invalid, task-failing or effect-failing result")
emit("case_decision", decision="task_checks_passed", numerical_review="pending",
release_acceptance="pending_owner_review", effects=copy.deepcopy(effects), answer=answer)
return {"state": "task_checks_passed_numerical_pending", "answer": answer,
"effects": effects, "observations": observations}
raise Hold("Bounded tool turns exhausted; record partial effects and hold")
except Exception as exc:
try:
emit("case_decision", decision="hold", reason=str(exc), effects=copy.deepcopy(effects))
except Hold:
pass # Journal outage is uncertain: reconcile before rerun.
if isinstance(exc, Hold):
raise
raise Hold("Unexpected case failure; reconcile exact journal/intents") from exc4. Connect the real application and measure each case
The following is the complete call and measurement wrapper. It is integration code, not a runnable sample with pretend customer inputs. Define every named value and callback against your real owned application before running it. A paid request happens only when run_case calls the authorized transport. The API key comes from private storage. captured_effective_api_version is the observed, supported value from the original route, supplied explicitly; the module does not silently upgrade it. Anthropic's current examples use 2023-06-01. Moving separately to API version 2026-10-21 needs its own case review and also rejects sampler keys at defaults. Versioning is independent of model selection.
import os
import hashlib
import json
import time
from direct_messages_path import build_request, direct_http_once, run_case
candidate, field_changes = build_request(
captured_old_request,
approved_case["messages"],
case_policy=approved_case["tool_policy"],
effort="medium", # owner evaluates this level against its frozen cases
)
case_identity = {
"case_id": approved_case["tool_policy"]["case_id"],
"code_revision": captured_code_revision,
"original_request_sha256": captured_old_request_sha256,
"api_version": captured_effective_api_version,
}
started_ns = time.monotonic_ns()
observed = run_case(
candidate,
case_policy=approved_case["tool_policy"],
identity=case_identity,
reconcile_prior=the_app_authoritative_prior_intent_reconciler,
transport=lambda req: direct_http_once(
req,
api_key=os.environ["ANTHROPIC_API_KEY"],
api_version=captured_effective_api_version,
),
execute_tool=the_app_authorized_tool_handler, # (case_id, tool_use_id, name, input)
validate_result=the_app_frozen_case_validator,
journal=the_app_private_durable_case_journal,
)
elapsed_ms = (time.monotonic_ns() - started_ns) / 1_000_000
handoff = {
"event": "numerical_review_pending",
"binding": {
**case_identity,
"candidate_request_sha256": hashlib.sha256(
json.dumps(candidate, sort_keys=True, separators=(",", ":")).encode()
).hexdigest(),
},
"task_state": observed["state"], # task_checks_passed_numerical_pending
"observed_elapsed_ms": elapsed_ms,
"turns": [
{"http": turn["http"], "response_id": turn["response_id"],
"usage_raw": turn["usage_raw"]}
for turn in observed["observations"]
],
"effect_receipts": observed["effects"],
"frozen_thresholds": approved_case["thresholds"],
"cost_and_latency_decision": "pending_owner_review",
}
the_app_private_durable_case_journal(handoff)The app supplies captured_old_request as its real parsed serialized body, captured_old_request_sha256 as the saved original bytes' hash, captured_code_revision, captured_effective_api_version, and approved_case with real user-ending messages, tool_policy and thresholds. It must also retain prompt, handler, validator and deployment revisions in its private case record. Inspect field_changes and the serialized candidate before the first call. Copying the wrapper without those values cannot run an application migration.
Four callbacks carry the owner's existing controls. the_app_authoritative_prior_intent_reconciler(binding) returns True only after checking that every earlier attempt and tool intent for this exact case is resolved. the_app_authorized_tool_handler(case_id, tool_use_id, name, input) enforces the real permission and schema, executes only the approved action, then returns {"content": ..., "effect_state": "confirmed" or "none", "receipt": ...} from independent effect readback. the_app_frozen_case_validator(answer, effects) checks the real answer, JSON schema where used, task meaning and required or forbidden effects, returning only True for success. the_app_private_durable_case_journal(event) commits each private event durably or raises. These callbacks are application-specific; a placeholder implementation would erase the acceptance evidence.
The journal commits request_attempt before transport; full response or request_error; tool_intent before each handler; tool_receipt or tool_unknown after it; and a case_decision. Each event has the case, original and candidate request hashes, code revision, API version and model. Retain raw response bytes or a protected private copy, redacted for later sharing. direct_http_once returns observed HTTP status and request-id (or x-request-id) separately from the Messages response id; a missing header stays null. A custom transport returning parsed JSON alone leaves HTTP metadata unknown. The actual transport must record every retry or fallback if the wider app has one; this reference has neither.
The parser checks the whole returned set of tool calls for unique IDs, allowed names, count, parallel authority and parseable JSON input before the first effect. It preserves ordered thinking blocks in the assistant continuation and sends matching tool_result blocks in the next user message. The handler still checks application authorization and schema. An effect is uncertain after a lost receipt or journal outage: reconcile the exact external effect before considering a rerun.
For a final answer, the runner requires end_turn, nonempty text, the owner's validator and every required tool call. Refusal, max_tokens, model_context_window_exceeded, unexpected stop reason, empty text, invalid block or usage, missing required call and exhausted turns all produce HOLD. Raw input_tokens and output_tokens must be present nonnegative integers; boolean, missing and null values do not count as zero. Keep returned cache counters and every turn's separate HTTP and Messages IDs. In a streamed app, a different adapter must wait for terminal message_stop, assemble complete blocks and tool input, and retain usage before any effect. A broken stream has an unknown outcome here.
When the API returns 400, inspect the recorded redacted serialized body, exact error and effective version. Check even default-valued sampler fields, old thinking budgets, a final assistant prefill, forced any or named tool choice, custom tool schema strictness, the JSON format field and obsolete beta headers. Review current versions for any built-in tool path, including text editing and code execution, outside this reference. A tool string parameter with a trailing newline can matter to the real handler; compare it with the approved case rather than trimming it silently. A successful 200 establishes that the request was accepted, while the returned task, effect, usage and numerical records still decide whether the application path works.
5. Decide on observed work and a safe release
task_checks_passed_numerical_pending is never a final pass. The monotonic wrapper records elapsed time and hands off per-turn raw usage, effect receipts and the frozen thresholds. The owner must apply current applicable billing terms to actual usage, cache, tool, retry and fallback evidence; record each observed cost and latency value as pass, fail or unknown against its preapproved threshold; and make a separate bound accept-or-HOLD decision. Anthropic's Sonnet 5.5 overview currently lists direct API rates of $2 per million input tokens and $10 per million output tokens, with 1 million tokens of context and up to 128,000 output tokens. The actual account route and charge need verification. We have no measured savings or latency claim. A token estimate is not a bill.
Only the exact code, configuration, prompt, model, effort, API version and request revision that passes every applicable task, effect, schema and numerical case can go to the release owner. The owner reviews the staged evidence, explicitly authorizes activation and checks the first authorized traffic. An untested, failed or unknown case stays HOLD, with the precise case, evidence gap and separately verified safe service state recorded. The retired Sonnet 4 Claude API ID cannot be the rollback plan. A repair changes one concrete defect under a new revision, then repeats the affected and representative regression cases before a new release decision.
Sources & further reading
- Anthropic's Sonnet 5.5 migration guide sets out the cumulative request, thinking, tool and response changes for a Sonnet 4 starting point. It does not verify any reader application.
- Anthropic's model deprecations records the Sonnet 4 Claude API retirement and recommended replacement, while distinguishing partner-operated lifecycles.
- Sonnet 5.5 overview supplies the listed model ID, limits and direct API prices. It does not establish an account's access or actual bill.
- Anthropic API versioning explains the separately effective
anthropic-versionheader, including SDK selection. - Messages API reference documents request fields and typed response and stop contracts that the application must validate.



