#!/usr/bin/env python3 """Robust Anthropic error handling (Python SDK): typed exceptions, retry matrix, request ids, streaming errors. STATUS: LIVE_VERIFIED 2026-09-18 (deliberate 404 + 400 on claude-haiku-4-5-20251001; nothing billed). Run: .venv/bin/python examples/shared/errors/anthropic_error_handling.py """ import random import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[3])) from scripts import live # noqa: E402 import anthropic # noqa: E402 # max_retries=0 so the demo shows the raw first failure; production: keep the default 2 (SDK retries 408/409/429/5xx + connection errors, honors retry-after). client = anthropic.Anthropic(max_retries=0, timeout=60) MODEL = "claude-haiku-4-5-20251001" RETRYABLE = (anthropic.RateLimitError, anthropic.InternalServerError, anthropic.APIConnectionError, anthropic.APITimeoutError) # 429, >=500 (incl. 529 OverloadedError), network FATAL = (anthropic.BadRequestError, anthropic.AuthenticationError, anthropic.PermissionDeniedError, anthropic.NotFoundError, anthropic.UnprocessableEntityError) # 400/401/403/404/422 — fix the request def call_with_backoff(fn, attempts=4, base=0.5, cap=8.0): for i in range(attempts): try: return fn() except anthropic.APIStatusError as e: should_retry = e.response.headers.get("x-should-retry") # live: 'false' on every 4xx retry_after = e.response.headers.get("retry-after") # seconds, absent on spend-cap 429 if isinstance(e, FATAL) or should_retry == "false" or i == attempts - 1: raise delay = float(retry_after) if retry_after else min(cap, base * 2 ** i) * (1 + random.random() * 0.25) print(f" retryable {e.status_code} {type(e).__name__} (request-id {e.request_id}); sleeping {delay:.2f}s") time.sleep(delay) except RETRYABLE as e: # connection/timeouts have no status if i == attempts - 1: raise time.sleep(min(cap, base * 2 ** i)) def demo(label, fn): try: r = call_with_backoff(fn) print(f"{label}: ok -> {r}") except anthropic.APIStatusError as e: body = e.body if isinstance(e.body, dict) else {} err = body.get("error", {}) print(f"{label}: HTTP {e.status_code} {type(e).__name__} type={err.get('type')} msg={err.get('message')!r} request_id={e.request_id}") live.log_request("anthropic", "POST", "/v1/messages", e.status_code, 0, f"error-handling demo {label}") except anthropic.APIConnectionError as e: # includes APITimeoutError print(f"{label}: connection problem {type(e).__name__}: {e}") demo("404 unknown model", lambda: client.messages.create(model="claude-does-not-exist", max_tokens=8, messages=[{"role": "user", "content": "Reply with OK."}])) demo("400 bad temperature", lambda: client.messages.create(model=MODEL, max_tokens=8, extra_body={"temperature": 5}, messages=[{"role": "user", "content": "Reply with OK."}])) # NOTE: anthropic 1.7.0 removed the deprecated `temperature`/`top_p`/`top_k` keyword arguments from messages.create(); # send them with extra_body={...} if you still target a model that accepts them. # Streaming: an `error` event can arrive after HTTP 200; the SDK raises inside the iteration. def stream_demo(): try: with client.messages.stream(model=MODEL, max_tokens=8, messages=[{"role": "user", "content": "Reply with OK."}]) as s: text = "".join(s.text_stream) print("stream ok:", text.strip(), "| stop_reason:", s.get_final_message().stop_reason) live.log_request("anthropic", "POST", "/v1/messages", 200, 0.00004, "error-handling demo stream") except anthropic.APIError as e: # mid-stream overloaded_error etc. → retry the whole request with backoff print("stream failed:", type(e).__name__, e) stream_demo()