SPB Git

spb/wp45_uqo

Public
4.3 KB · 68 lines python
1"""Thin Claude API wrapper: structured outputs, provenance ledger, cost accounting, budget cap, response cache."""2from __future__ import annotations3import json, time, hashlib, threading4from pathlib import Path5import anthropic6from . import config7from .provenance import Ledger, sha256_text, now_iso, code_version, ROOT89_lock = threading.Lock()10LEDGER = Ledger(ROOT / "data" / "annotations" / "api_ledger.jsonl")11CACHE_DIR = ROOT / "data" / "annotations" / "_llm_cache"; CACHE_DIR.mkdir(parents=True, exist_ok=True)1213def spent_usd() -> float:14    return round(sum(r.get("cost_usd", 0.0) for r in LEDGER.read()), 4)1516def cost_of(model: str, usage) -> float:17    pr = config.load("models")["pricing"].get(model, {"input": 4.0, "output": 20.0, "cache_read": 0.2, "cache_write": 5.0})18    cr = getattr(usage, "cache_read_input_tokens", 0) or 0; cw = getattr(usage, "cache_creation_input_tokens", 0) or 019    return (usage.input_tokens * pr["input"] + usage.output_tokens * pr["output"] + cr * pr["cache_read"] + cw * pr["cache_write"]) / 1e62021class BudgetExceeded(RuntimeError): pass2223def extract_json(text: str):24    """Parse a JSON object from model text (tolerates code fences / surrounding prose)."""25    t = text.strip()26    if t.startswith("```"): t = t.split("\n", 1)[1] if "\n" in t else t; t = t.rsplit("```", 1)[0]27    try: return json.loads(t)28    except json.JSONDecodeError: pass29    i, j = t.find("{"), t.rfind("}")30    if i >= 0 and j > i:31        try: return json.loads(t[i:j + 1])32        except json.JSONDecodeError: return None33    return None3435def call(system: str, user: str, *, model: str | None = None, schema: dict | None = None, max_tokens: int = 8000, effort: str = "medium",36         purpose: str = "", prompt_id: str = "", prompt_version: str = "", use_cache: bool = True, meta: dict | None = None, parse_json: bool = False) -> dict:37    """One Messages API call. Returns {"text", "json", "usage", "cost_usd", "cached", "request_id", "model"}; logs to the ledger."""38    cfg = config.load("models"); model = model or cfg["annotation_model"]39    key = sha256_text(json.dumps({"m": model, "s": system, "u": user, "schema": schema, "eff": effort, "mt": max_tokens}, sort_keys=True))40    cp = CACHE_DIR / f"{key}.json"41    if use_cache and cp.exists():42        d = json.loads(cp.read_text()); d["cached"] = True; return d43    with _lock:44        if spent_usd() >= cfg["budget_usd_cap"]: raise BudgetExceeded(f"budget cap {cfg['budget_usd_cap']} USD reached (spent {spent_usd()})")45    client = anthropic.Anthropic(api_key=config.api_key(), max_retries=3, timeout=600)46    kw = dict(model=model, max_tokens=max_tokens, system=[{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}],47              messages=[{"role": "user", "content": user}], output_config={"effort": effort})48    if schema: kw["output_config"]["format"] = {"type": "json_schema", "schema": schema}49    t0 = time.time(); last = None50    for attempt in range(3):51        try:52            r = client.messages.create(**kw); break53        except (anthropic.RateLimitError, anthropic.APIStatusError, anthropic.APIConnectionError) as e:54            last = e; time.sleep(5 * (attempt + 1))55    else:56        raise last57    text = "".join(b.text for b in r.content if b.type == "text")58    out = {"text": text, "json": None, "model": r.model, "stop_reason": r.stop_reason, "latency_s": round(time.time() - t0, 2),59           "usage": {"input": r.usage.input_tokens, "output": r.usage.output_tokens, "cache_read": getattr(r.usage, "cache_read_input_tokens", 0) or 0, "cache_write": getattr(r.usage, "cache_creation_input_tokens", 0) or 0},60           "cost_usd": round(cost_of(model, r.usage), 6), "request_id": getattr(r, "_request_id", None), "cached": False, "ts": now_iso(), "effort": effort}61    if schema or parse_json:62        out["json"] = extract_json(text)63    if r.stop_reason == "refusal": out["refusal"] = getattr(r, "stop_details", None) and r.stop_details.category64    LEDGER.append({"purpose": purpose, "prompt_id": prompt_id, "prompt_version": prompt_version, "model": r.model, "effort": effort, "usage": out["usage"], "cost_usd": out["cost_usd"],65                   "latency_s": out["latency_s"], "request_id": out["request_id"], "stop_reason": r.stop_reason, "cache_key": key, "code_version": code_version(), **(meta or {})})66    if use_cache: cp.write_text(json.dumps(out, ensure_ascii=False))67    return out68
Files · f48490eca784017968beb6af9e04c942bd1390d9
Menu

Theme