SPB Git

spb/wpai0002_uqo

Public
5.8 KB · 113 lines python
1"""Anthropic Claude adapter: pinned model id, logged request id / usage / stop reason / cost, exponential2backoff, refusal handling, and Message Batches support (50 % price). Prices come from configs/rewriting/pricing.json3and are never assumed. Thinking is adaptive by default on Claude 5.x models; we only set ``output_config.effort``.4"""5from __future__ import annotations67import json8import time9from pathlib import Path1011from htr.rewrite.provider import Provider, ProviderResult, Timer1213ROOT = Path(__file__).resolve().parents[3]141516def load_pricing(path: Path | None = None) -> dict:17    p = path or ROOT / "configs/rewriting/pricing.json"18    return json.loads(Path(p).read_text())192021def estimate_cost(model: str, usage: dict, pricing: dict, batch: bool = False) -> float | None:22    m = pricing["models"].get(model)23    if not m:24        return None25    disc = pricing.get("batch_discount", 0.5) if batch else 1.026    inp = usage.get("input_tokens", 0) or 027    out = usage.get("output_tokens", 0) or 028    cr = usage.get("cache_read_input_tokens", 0) or 029    cw = usage.get("cache_creation_input_tokens", 0) or 030    return disc * (inp * m["input"] + cw * m["input"] * 1.25 + cr * m.get("cache_read", m["input"] * 0.1) + out * m["output"]) / 1e6313233class AnthropicProvider(Provider):34    name = "anthropic"3536    def __init__(self, model: str, effort: str = "low", pricing: dict | None = None, max_retries: int = 6, api_key: str | None = None):37        import anthropic3839        self.model = model40        self.effort = effort41        self.pricing = pricing or load_pricing()42        if model not in self.pricing["models"]:43            raise ValueError(f"model {model!r} missing from configs/rewriting/pricing.json — add verified prices first")44        self.client = anthropic.Anthropic(api_key=api_key, max_retries=2)45        self.max_retries = max_retries46        self._anthropic = anthropic4748    # ---- live, single request --------------------------------------------------------------------------49    def rewrite(self, system: str, messages: list[dict], max_tokens: int, params: dict) -> ProviderResult:50        a = self._anthropic51        for attempt in range(self.max_retries + 1):52            try:53                with Timer() as t:54                    resp = self.client.messages.with_raw_response.create(55                        model=self.model, max_tokens=max_tokens, system=system, messages=messages, output_config={"effort": self.effort}56                    )57                msg = resp.parse()58                req_id = resp.headers.get("request-id")59                return self._to_result(msg, req_id, t.elapsed, batch=False)60            except a.RateLimitError:61                time.sleep(min(90.0, 5.0 * 2**attempt))62            except (a.APIConnectionError, a.InternalServerError, a.APIStatusError) as e:  # noqa: PERF20363                if isinstance(e, a.APIStatusError) and e.status_code < 500 and e.status_code not in (408, 409, 429):64                    return ProviderResult(None, "error", self.model, error=f"{type(e).__name__}: {str(e)[:300]}")65                time.sleep(min(90.0, 5.0 * 2**attempt))66        return ProviderResult(None, "error", self.model, error="retries exhausted")6768    def _to_result(self, msg, req_id: str | None, latency: float | None, batch: bool) -> ProviderResult:69        usage = {k: getattr(msg.usage, k, None) for k in ("input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens")}70        text = "".join(b.text for b in msg.content if getattr(b, "type", None) == "text").strip()71        if msg.stop_reason == "refusal":72            status = "refused"73        elif msg.stop_reason == "max_tokens":74            status = "truncated"75        elif not text:76            status = "empty"77        else:78            status = "ok"79        return ProviderResult(text or None, status, msg.model, provider_request_id=req_id or getattr(msg, "id", None), usage=usage, cost_usd=estimate_cost(self.model, usage, self.pricing, batch), stop_reason=msg.stop_reason, latency_s=latency)8081    # ---- Message Batches -------------------------------------------------------------------------------------82    def supports_batch(self) -> bool:83        return True8485    def submit_batch(self, items: list[tuple[str, str, list[dict], int]]) -> str:86        """items: (custom_id, system, messages, max_tokens). Returns batch id."""87        from anthropic.types.message_create_params import MessageCreateParamsNonStreaming88        from anthropic.types.messages.batch_create_params import Request8990        reqs = [Request(custom_id=cid, params=MessageCreateParamsNonStreaming(model=self.model, max_tokens=mt, system=sy, messages=ms, output_config={"effort": self.effort})) for cid, sy, ms, mt in items]91        b = self.client.messages.batches.create(requests=reqs)92        return b.id9394    def batch_status(self, batch_id: str) -> dict:95        b = self.client.messages.batches.retrieve(batch_id)96        return {"id": b.id, "status": b.processing_status, "counts": b.request_counts.model_dump() if hasattr(b.request_counts, "model_dump") else dict(b.request_counts)}9798    def batch_results(self, batch_id: str) -> dict[str, ProviderResult]:99        out: dict[str, ProviderResult] = {}100        for r in self.client.messages.batches.results(batch_id):101            rt = r.result.type102            if rt == "succeeded":103                out[r.custom_id] = self._to_result(r.result.message, getattr(r.result.message, "id", None), None, batch=True)104            elif rt == "errored":105                err = r.result.error106                out[r.custom_id] = ProviderResult(None, "error", self.model, error=f"batch:{getattr(err, 'type', 'error')}")107            else:108                out[r.custom_id] = ProviderResult(None, "error", self.model, error=f"batch:{rt}")109        return out110111    def cancel_batch(self, batch_id: str) -> None:112        self.client.messages.batches.cancel(batch_id)113
Files · main
Menu

Theme