"""Track D: hosted APIs (Anthropic, OpenAI) with observed usage/cost accounting and documented prompt caching only. Keys are read from the environment or sibling .env files; never logged.""" from __future__ import annotations import os, re, time, json from pathlib import Path # Prices in USD per million tokens, from the provider pricing pages fetched 2026-10-29 (literature/audit_group3.md) — # estimated cost, labelled 'estimated' in records. PRICES = { "claude-haiku-4-5": {"input": 1.00, "output": 5.00, "cache_write_5m": 1.25, "cache_read": 0.10, "source": "platform.claude.com pricing table, 2026-10-29"}, "gpt-5-nano": {"input": 0.05, "output": 0.40, "cached_input": 0.005, "source": "openai.com/api/pricing, as known 2026-10 (verify)"}, } def _load_key(name: str) -> str | None: if os.environ.get(name): return os.environ[name] for p in [Path(__file__).resolve().parents[2] / ".env", Path.home() / "Desktop/Recherche-UQO/immobilier/agentic-valuation-quebec/.env"]: if p.exists(): m = re.search(rf"^{name}=(.*)$", p.read_text(), re.M) if m: return m.group(1).strip().strip('"').strip("'") return None class Budget: def __init__(self, cap_usd: float, ledger_path: str): self.cap = cap_usd; self.path = Path(ledger_path); self.spent = 0.0 if self.path.exists(): for line in self.path.read_text().splitlines(): try: self.spent += json.loads(line)["cost_usd"] except Exception: pass def charge(self, cost: float, meta: dict): self.spent += cost self.path.parent.mkdir(parents=True, exist_ok=True) with open(self.path, "a") as f: f.write(json.dumps({"cost_usd": cost, **meta}) + "\n") def exhausted(self) -> bool: return self.spent >= self.cap def anthropic_answer(system: str, user_blocks: list[dict], model: str = "claude-haiku-4-5", max_tokens: int = 128, use_cache_control: bool = False) -> dict: import anthropic client = anthropic.Anthropic(api_key=_load_key("ANTHROPIC_API_KEY")) content = [] for i, b in enumerate(user_blocks): blk = {"type": "text", "text": b["text"]} if use_cache_control and b.get("cache"): blk["cache_control"] = {"type": "ephemeral"} content.append(blk) t0 = time.perf_counter(); first = None; text = "" with client.messages.stream(model=model, max_tokens=max_tokens, temperature=0.0, system=system, messages=[{"role": "user", "content": content}]) as stream: for ev in stream.text_stream: if first is None and ev: first = time.perf_counter() text += ev final = stream.get_final_message() t1 = time.perf_counter() u = final.usage pr = PRICES[model] cost = (u.input_tokens * pr["input"] + u.output_tokens * pr["output"] + (u.cache_creation_input_tokens or 0) * pr["cache_write_5m"] + (u.cache_read_input_tokens or 0) * pr["cache_read"]) / 1e6 return {"text": text, "model": final.model, "input_tokens": u.input_tokens, "output_tokens": u.output_tokens, "cache_creation_input_tokens": u.cache_creation_input_tokens, "cache_read_input_tokens": u.cache_read_input_tokens, "ttft_ms": ((first or t1) - t0) * 1000, "total_ms": (t1 - t0) * 1000, "cost_usd_estimated": cost, "price_source": pr["source"]} def openai_answer(system: str, user_text: str, model: str = "gpt-5-nano", max_tokens: int = 128) -> dict: import urllib.request key = _load_key("OPENAI_API_KEY") body = {"model": model, "input": [{"role": "developer", "content": system}, {"role": "user", "content": user_text}], "max_output_tokens": max_tokens, "reasoning": {"effort": "minimal"}} req = urllib.request.Request("https://api.openai.com/v1/responses", data=json.dumps(body).encode(), headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}) t0 = time.perf_counter() with urllib.request.urlopen(req, timeout=120) as r: d = json.load(r) t1 = time.perf_counter() text = "".join(c.get("text", "") for o in d.get("output", []) for c in o.get("content", []) if c.get("type") == "output_text") u = d.get("usage", {}) cached = (u.get("input_tokens_details") or {}).get("cached_tokens", 0) pr = PRICES[model] cost = ((u.get("input_tokens", 0) - cached) * pr["input"] + cached * pr["cached_input"] + u.get("output_tokens", 0) * pr["output"]) / 1e6 return {"text": text, "model": d.get("model"), "input_tokens": u.get("input_tokens"), "output_tokens": u.get("output_tokens"), "cached_tokens": cached, "ttft_ms": None, "total_ms": (t1 - t0) * 1000, "cost_usd_estimated": cost, "price_source": pr["source"]}