"""OpenRouter model pricing cache + cost computation. v0.1.0: in-process dict cache + optional DB refresh. doctor와 background refresh가 업데이트 trigger (Step 12). """ from __future__ import annotations from dataclasses import dataclass import httpx from ..errors import MyDeepAgentError @dataclass(frozen=True) class ModelPrice: model: str # OpenRouter id, e.g. "deepseek/deepseek-chat" input_per_1k_usd: float output_per_1k_usd: float context_length: int class PricingCache: """In-memory cache of OpenRouter pricing. Caller refreshes via fetch_openrouter_pricing().""" def __init__(self) -> None: self._cache: dict[str, ModelPrice] = {} def get(self, model: str) -> ModelPrice | None: key = model.removeprefix("openrouter:") return self._cache.get(key) def set(self, prices: list[ModelPrice]) -> None: for p in prices: self._cache[p.model] = p def compute_cost(self, model: str, input_tokens: int, output_tokens: int) -> float: """Return USD cost. Returns 0.0 if model price is unknown (logged separately).""" price = self.get(model) if price is None: return 0.0 return (input_tokens / 1000.0) * price.input_per_1k_usd + ( output_tokens / 1000.0 ) * price.output_per_1k_usd async def fetch_openrouter_pricing(api_key: str, base_url: str) -> list[ModelPrice]: """Fetch the OpenRouter /models endpoint and parse pricing.""" async with httpx.AsyncClient(timeout=10.0) as client: try: r = await client.get( f"{base_url}/models", headers={"Authorization": f"Bearer {api_key}"}, ) r.raise_for_status() except httpx.HTTPError as e: raise MyDeepAgentError.recoverable( "network_blip", message=f"failed to fetch openrouter pricing: {e}", cause=e, ) from e data: dict[str, object] = r.json() return _parse_pricing_payload(data) def _parse_pricing_payload(data: dict[str, object]) -> list[ModelPrice]: """Parse OpenRouter response. Expected format:: {"data": [{"id": "...", "pricing": {"prompt": "...", "completion": "..."}, ...}]} """ models = data.get("data", []) if not isinstance(models, list): return [] out: list[ModelPrice] = [] for m in models: if not isinstance(m, dict): continue model_id = m.get("id") pricing = m.get("pricing") or {} if not isinstance(model_id, str) or not isinstance(pricing, dict): continue try: prompt_per_token = float(pricing.get("prompt", "0") or "0") completion_per_token = float(pricing.get("completion", "0") or "0") ctx_len = int(m.get("context_length", 0) or 0) except (TypeError, ValueError): continue out.append( ModelPrice( model=model_id, input_per_1k_usd=prompt_per_token * 1000.0, output_per_1k_usd=completion_per_token * 1000.0, context_length=ctx_len, ) ) return out