"""Anthropic adapter (official `anthropic` SDK, Messages API with structured output). Reads ANTHROPIC_API_KEY from the environment only. The frozen system prompt is marked for prompt caching; the user message carries the volatile market snapshot. `effort` maps to output_config.effort for models that support it (Claude 4.6+). No thinking parameter is sent (adaptive by default).""" from __future__ import annotations import os import time from . import ProviderResult from ..schema import DECISION_JSON_SCHEMA class AnthropicProvider: name = "anthropic" def __init__(self, timeout_s: float = 60.0, max_retries: int = 3, **_: object) -> None: import anthropic if not os.environ.get("ANTHROPIC_API_KEY"): raise RuntimeError("ANTHROPIC_API_KEY is not set") self._anthropic = anthropic self.client = anthropic.Anthropic(timeout=timeout_s, max_retries=max_retries) def complete(self, system: str, user: str, model: str, reasoning_effort: str | None = "low", max_output_tokens: int = 400, **_: object) -> ProviderResult: t0 = time.perf_counter() kwargs: dict = dict( model=model, max_tokens=max(max_output_tokens, 256), system=[{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}], messages=[{"role": "user", "content": user}], output_config={"format": {"type": "json_schema", "schema": DECISION_JSON_SCHEMA}}, ) if reasoning_effort: kwargs["output_config"]["effort"] = reasoning_effort try: resp = self.client.messages.create(**kwargs) except self._anthropic.RateLimitError as e: return ProviderResult(text="", error=f"rate_limit: {e}", latency_s=time.perf_counter() - t0, model=model) except self._anthropic.APIStatusError as e: return ProviderResult(text="", error=f"status_{e.status_code}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model) except self._anthropic.APIConnectionError as e: return ProviderResult(text="", error=f"connection: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model) except Exception as e: # noqa: BLE001 return ProviderResult(text="", error=f"other: {type(e).__name__}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model) if getattr(resp, "stop_reason", "") == "refusal": return ProviderResult(text="", error="refusal", latency_s=time.perf_counter() - t0, model=model) text = "".join(b.text for b in resp.content if getattr(b, "type", "") == "text") u = resp.usage return ProviderResult(text=text, input_tokens=int(u.input_tokens or 0), cached_input_tokens=int(getattr(u, "cache_read_input_tokens", 0) or 0), output_tokens=int(u.output_tokens or 0), latency_s=time.perf_counter() - t0, model=model, request_id=getattr(resp, "_request_id", "") or "", error="" if text else "empty_output")