1"""Anthropic adapter (official `anthropic` SDK, Messages API with structured output).23Reads ANTHROPIC_API_KEY from the environment only. The frozen system prompt is marked for prompt4caching; the user message carries the volatile market snapshot. `effort` maps to output_config.effort5for models that support it (Claude 4.6+). No thinking parameter is sent (adaptive by default)."""6from __future__ import annotations78import os9import time1011from . import ProviderResult12from ..schema import DECISION_JSON_SCHEMA131415class AnthropicProvider:16 name = "anthropic"1718 def __init__(self, timeout_s: float = 60.0, max_retries: int = 3, **_: object) -> None:19 import anthropic20 if not os.environ.get("ANTHROPIC_API_KEY"):21 raise RuntimeError("ANTHROPIC_API_KEY is not set")22 self._anthropic = anthropic23 self.client = anthropic.Anthropic(timeout=timeout_s, max_retries=max_retries)2425 def complete(self, system: str, user: str, model: str, reasoning_effort: str | None = "low",26 max_output_tokens: int = 400, **_: object) -> ProviderResult:27 t0 = time.perf_counter()28 kwargs: dict = dict(29 model=model,30 max_tokens=max(max_output_tokens, 256),31 system=[{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}],32 messages=[{"role": "user", "content": user}],33 output_config={"format": {"type": "json_schema", "schema": DECISION_JSON_SCHEMA}},34 )35 if reasoning_effort:36 kwargs["output_config"]["effort"] = reasoning_effort37 try:38 resp = self.client.messages.create(**kwargs)39 except self._anthropic.RateLimitError as e:40 return ProviderResult(text="", error=f"rate_limit: {e}", latency_s=time.perf_counter() - t0, model=model)41 except self._anthropic.APIStatusError as e:42 return ProviderResult(text="", error=f"status_{e.status_code}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)43 except self._anthropic.APIConnectionError as e:44 return ProviderResult(text="", error=f"connection: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)45 except Exception as e: # noqa: BLE00146 return ProviderResult(text="", error=f"other: {type(e).__name__}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)47 if getattr(resp, "stop_reason", "") == "refusal":48 return ProviderResult(text="", error="refusal", latency_s=time.perf_counter() - t0, model=model)49 text = "".join(b.text for b in resp.content if getattr(b, "type", "") == "text")50 u = resp.usage51 return ProviderResult(text=text, input_tokens=int(u.input_tokens or 0),52 cached_input_tokens=int(getattr(u, "cache_read_input_tokens", 0) or 0),53 output_tokens=int(u.output_tokens or 0), latency_s=time.perf_counter() - t0,54 model=model, request_id=getattr(resp, "_request_id", "") or "",55 error="" if text else "empty_output")56