Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/akash-sathish8_arceo.json
T

1 line
83 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "akash-sathish8/arceo", "files": {"backend/analysis/spend_forecast.py": {"1294c09cc367e4a1df18f8aa62c6d265b4f89c18": {"t": 1785674500.6455212, "n": 80065, "h": "e830ef187f8f1e3c", "c": "\"\"\"Operational spend forecaster.\n\nGiven an agent's capability tree + optional sandbox traces + optional user\noverrides, produces a monthly cost forecast at one of three confidence tiers\n(low / medium / high) based on data availability.\n\nFormula and methodology: brain/Signals/Cost calculation methodology.md\nCalibration sources: brain/Signals/Cost forecaster calibration data.md\n\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nimport json\nfrom pathlib import Path\nfrom typing import Any, Optional\n\ntry:\n import yaml\nexcept ImportError:\n yaml = None # type: ignore\n\n# ── Config load ──────────────────────────────────────────────────────────────\n\n_DEFAULTS_PATH = Path(__file__).parent / \"cost_defaults_operational.yaml\"\n# Generated from the YAML by scripts/gen_cost_defaults_fallback.py; kept in\n# sync by tests/test_price_hygiene.py. Never edit by hand.\n_FALLBACK_JSON_PATH = Path(__file__).parent / \"cost_defaults_operational.fallback.json\"\n_DEFAULTS_CACHE: Optional[dict] = None\n\n\ndef load_defaults(org_id: Optional[str] = None) -> dict:\n \"\"\"Load and cache the operational cost defaults YAML.\n\n With `org_id`, returns a deep copy with that org's `cost_overrides` rows\n merged on top (a customer's negotiated model rates, custom tool prices,\n infra overhead). The module cache only ever holds the pristine YAML — the\n per-org merge happens after the cache read and merged copies are never\n cached, so one org's overrides can't leak into another's forecasts.\n \"\"\"\n global _DEFAULTS_CACHE\n if _DEFAULTS_CACHE is None:\n if yaml is None:\n # No pyyaml: load the GENERATED JSON twin of the YAML (see\n # scripts/gen_cost_defaults_fallback.py). The old hand-maintained\n # fallback dict had drifted to a fraction of the catalog with\n # stale rates — generation + a sync test make that impossible.\n with open(_FALLBACK_JSON_PATH) as f:\n _DEFAULTS_CACHE = json.load(f)\n else:\n with open(_DEFAULTS_PATH) as f:\n _DEFAULTS_CACHE = yaml.safe_load(f)\n if not org_id:\n return _DEFAULTS_CACHE\n rows = _fetch_org_overrides(org_id)\n org_default_model = _fetch_org_default_model(org_id)\n if not rows and not org_default_model:\n return _DEFAULTS_CACHE\n import copy\n merged = copy.deepcopy(_DEFAULTS_CACHE)\n for scope, key, sub_key, value in rows:\n if scope == \"model\":\n # Only patch models that exist — a partial model dict (an override\n # for a key with no base pricing) can't be priced.\n if key in merged.get(\"models\", {}):\n merged[\"models\"][key][sub_key] = value\n elif scope == \"tool\":\n merged.setdefault(\"tool_action_costs\", {}).setdefault(key, {})[sub_key] = value\n elif scope == \"infra\":\n merged.setdefault(\"infrastructure\", {})[key] = value\n # Per-org default model: only honor a recognized one (else keep the YAML\n # default rather than pricing at an unknown model).\n if org_default_model and _model_recognized(org_default_model, merged):\n merged[\"default_model\"] = _resolve_model_key(org_default_model, merged)\n return merged\n\n\ndef _fetch_org_default_model(org_id: str) -> Optional[str]:\n \"\"\"Read an org's configured default model from workspace_settings, or None.\"\"\"\n try:\n from db import get_db\n with get_db() as conn:\n row = conn.execute(\n \"SELECT default_model FROM workspace_settings WHERE org_id = %s\",\n (org_id,),\n ).fetchone()\n return (row[\"default_model\"] or None) if row else None\n except Exception:\n return None\n\n\ndef _fetch_org_overrides(org_id: str) -> list[tuple]:\n \"\"\"Read an org's cost_overrides rows. Returns [] on any failure (missing\n table, no DB) so the forecaster degrades to pristine defaults.\"\"\"\n try:\n from db import get_db\n with get_db() as conn:\n rows = conn.execute(\n \"SELECT scope, key, sub_key, value FROM cost_overrides WHERE org_id = %s\",\n (org_id,),\n ).fetchall()\n return [(r[\"scope\"], r[\"key\"], r[\"sub_key\"], float(r[\"value\"])) for r in rows]\n except Exception:\n return []\n\n\ndef clear_override_caches() -> None:\n \"\"\"Bust derived caches after a cost_overrides write. The defaults cache\n itself holds only pristine YAML and never needs busting.\"\"\"\n _SENSITIVITY_CACHE.clear()\n\n\ndef _first_set(*vals, default):\n \"\"\"First value that is not None, else default. Unlike `a or b`, a legitimate\n 0 (e.g. a paused agent at 0 calls/day) is honored rather than treated as\n unset and silently bumped to the default.\"\"\"\n for v in vals:\n if v is not None:\n return v\n return default\n\n\n# ── Unit economics — picked by tool mix ──────────────────────────────────────\n# LABELS ONLY. There are deliberately NO hardcoded $ values here — the per-outcome\n# dollar figure is computed per-agent from REAL sandbox-observed outcome counts in\n# `_compute_unit_econ`, or shown as \"not measured\" (null). No canned $.\n\n_UNIT_ECON_SETS = {\n \"support\": [\n {\"label\": \"Per refund processed\"},\n {\"label\": \"Per ticket resolved\"},\n {\"label\": \"Per customer touched\"},\n {\"label\": \"Per email sent\"},\n ],\n \"sales\": [\n {\"label\": \"Per lead enriched\"},\n {\"label\": \"Per outbound email\"},\n {\"label\": \"Per meeting booked\"},\n {\"label\": \"Per opportunity created\"},\n ],\n \"devops\": [\n {\"label\": \"Per deploy\"},\n {\"label\": \"Per incident triaged\"},\n {\"label\": \"Per rollback\"},\n {\"label\": \"Per alert routed\"},\n ],\n \"ops\": [\n {\"label\": \"Per record exported\"},\n {\"label\": \"Per batch run\"},\n {\"label\": \"Per file delivered\"},\n {\"label\": \"Per quality check\"},\n ],\n}\n\n\ndef _pick_unit_econ_set(agent_config: dict) -> list[dict]:\n \"\"\"Pick the unit-economics row LABELS that fit the agent's tool mix.\n Dollar values are computed per-agent (or left null) by `_compute_unit_econ`.\n \"\"\"\n tool_names = {(t.get(\"name\") or \"\").lower() for t in agent_config.get(\"tools\", [])}\n if {\"github\", \"pagerduty\", \"aws\", \"aws_ec2\", \"aws_ecs\", \"aws_rds\", \"aws_lambda\"} & tool_names:\n return _UNIT_ECON_SETS[\"devops\"]\n if {\"hubspot\", \"salesforce\", \"calendly\", \"clearbit\"} & tool_names:\n return _UNIT_ECON_SETS[\"sales\"]\n if {\"snowflake\", \"aws_s3\", \"bigquery\"} & tool_names:\n return _UNIT_ECON_SETS[\"ops\"]\n return _UNIT_ECON_SETS[\"support\"]\n\n\n# Map each unit-econ row label to the tool.action patterns that count as one\n# such outcome. An agent only shows a row if it has at least one matching\n# action; otherwise the row is dropped (per-agent honesty over uniform layout).\n_UNIT_ECON_ACTIONS: dict[str, list[str]] = {\n # support\n \"Per refund processed\": [\"stripe.create_refund\"],\n \"Per ticket resolved\": [\"zendesk.update_ticket\", \"zendesk.close_ticket\"],\n \"Per customer touched\": [\"zendesk.get_user\", \"zendesk.create_user\", \"hubspot.get_contact\"],\n \"Per email sent\": [\"sendgrid.send_email\", \"sendgrid.send_template_email\", \"gmail.send_message\"],\n # sales\n \"Per lead enriched\": [\"clearbit.enrich\", \"hubspot.update_contact\"],\n \"Per outbound email\": [\"sendgrid.send_email\", \"sendgrid.send_template_email\", \"gmail.send_message\"],\n \"Per meeting booked\": [\"calendly.create_event\", \"calendly.create_invitee\"],\n \"Per opportunity created\": [\"salesforce.create_opportunity\", \"hubspot.create_deal\"],\n # devops\n \"Per deploy\": [\"github.create_deployment\", \"aws_ecs.update_service\", \"aws_lambda.update_function_code\"],\n \"Per incident triaged\": [\"pagerduty.update_incident\", \"pagerduty.acknowledge_incident\"],\n \"Per rollback\": [\"github.revert_deployment\", \"aws_ecs.update_service\"],\n \"Per alert routed\": [\"pagerduty.create_incident\"],\n # ops\n \"Per record exported\": [\"aws_s3.put_object\", \"snowflake.unload\"],\n \"Per batch run\": [\"snowflake.execute_query\", \"bigquery.run_query\"],\n \"Per file delivered\": [\"aws_s3.put_object\"],\n \"Per quality check\": [\"snowflake.execute_query\"],\n}\n\n\ndef _agent_action_set(agent_config: dict) -> set[str]:\n \"\"\"Flatten an agent's tools/actions into {tool.action} strings.\"\"\"\n result: set[str] = set()\n for tool in agent_config.get(\"tools\", []):\n tool_name = (tool.get(\"name\") or \"\").lower()\n if not tool_name:\n continue\n for action_obj in tool.get(\"actions\", []):\n if isinstance(action_obj, dict):\n action_name = action_obj.get(\"action\") or action_obj.get(\"name\") or \"\"\n else:\n action_name = str(action_obj)\n if action_name:\n result.add(f\"{tool_name}.{action_name}\")\n return result\n\n\ndef _count_action_occurrences(sandbox_traces: list, targets: set[str]) -> int:\n \"\"\"Count steps across all traces whose tool.action is in targets.\"\"\"\n total = 0\n for trace in sandbox_traces:\n for step in trace.get(\"steps\", []) or []:\n tool = (step.get(\"tool\") or \"\").lower()\n action = step.get(\"action\") or \"\"\n if action and f\"{tool}.{action}\" in targets:\n total += 1\n return total\n\n\ndef _format_unit_value(usd: float) -> str:\n \"\"\"Format a per-outcome USD value with precision sized by magnitude.\"\"\"\n if usd >= 1.0:\n return f\"${usd:,.2f}\"\n if usd >= 0.01:\n return f\"${usd:.2f}\"\n if usd >= 0.001:\n return f\"${usd:.3f}\"\n return f\"${usd:.4f}\"\n\n\ndef _compute_unit_econ(\n agent_config: dict,\n sandbox_traces: Optional[list],\n monthly_point: float,\n runs_per_day: int,\n) -> list[dict]:\n \"\"\"Per-agent unit-economics rows — honest, never fabricated.\n\n Per-outcome $ is computed ONLY from real outcome counts observed in sandbox\n traces. There is NO canned archetype $ and NO uniform-mix guess: a row the\n agent CAN produce shows `value: None` (\"not measured\") until a sandbox sweep\n counts real outcomes. `runs_per_day` is per-RUN volume (sandbox occurrences\n are per run).\"\"\"\n archetype_rows = _pick_unit_econ_set(agent_config)\n agent_actions = _agent_action_set(agent_config)\n num_traces = len(sandbox_traces) if sandbox_traces else 0\n total_monthly_runs = max(int(runs_per_day or 0), 0) * 30\n\n rows: list[dict] = []\n for row in archetype_rows:\n label = row[\"label\"]\n candidates = _UNIT_ECON_ACTIONS.get(label, [])\n matching = {a for a in candidates if a in agent_actions}\n if not matching:\n continue\n value = None # honest default: not measured yet\n if num_traces > 0 and monthly_point > 0 and total_monthly_runs > 0:\n occurrences = _count_action_occurrences(sandbox_traces, matching)\n if occurrences > 0:\n monthly_outcomes = (occurrences / num_traces) * total_monthly_runs\n if monthly_outcomes > 0:\n value = _format_unit_value(monthly_point / monthly_outcomes)\n rows.append({\"label\": label, \"value\": value})\n return rows\n\n\n# ── Tier detection ───────────────────────────────────────────────────────────\n\n# Below this many captured LLM calls in the trailing window, observed rolling\n# averages are too noisy to trust — fall back to the static/sandbox estimate,\n# and don't claim the high-confidence band.\nLIVE_TRACE_MIN_CALLS = 50\n\n# Call COUNT alone can't distinguish a month-shaped signal from a burst: 55\n# calls in one busy 20-minute demo used to extrapolate to a full month at\n# ±15%. HIGH additionally requires traffic on this many DISTINCT days in the\n# window; a burst that clears the call floor but not the day floor is honest\n# medium-band evidence, not high.\nLIVE_TRACE_MIN_ACTIVE_DAYS = 3\n\n# But early live traffic IS a real volume signal (D27): a zero-code customer's\n# first captured calls must unlock a forecast at the wide low/medium band, not\n# a \"no data\" screen that contradicts the data-sources panel. From this floor\n# up, live traces count toward availability and feed the rolling averages;\n# below it a monthly extrapolation would ride on a handful of calls, so we\n# show capture progress instead of a number. HIGH confidence still requires\n# LIVE_TRACE_MIN_CALLS.\nLIVE_TRACE_MIN_CALLS_FORECAST = 5\n\n# Bumped whenever the cost FORMULA changes scale (not just calibration values),\n# so vs-last-month doesn't show a bogus jump comparing across formula versions.\n# v2 (2026-06-19): runs × turns model — forecasts ~4× higher than v1.\nFORECAST_FORMULA_VERSION = 2\n\n\ndef _detect_tier(sandbox_traces: Optional[list], live_trace_count_7d: int = 0,\n model_recognized: bool = True,\n live_active_days: Optional[int] = None) -> str:\n \"\"\"Determine confidence tier from input availability.\n\n High tier (the tight ±15% band) requires enough live calls that the rolling\n averages are actually applied — claiming high confidence on a handful of\n calls while still using default inputs would be dishonest. Below that, a\n few live calls don't beat sandbox traces, so fall through to medium/low.\n\n An unrecognized model caps the tier at LOW no matter how much trace data\n exists: the dominant cost driver (the per-token price) is a guess, and no\n volume of traffic makes a guessed price trustworthy. The check lives HERE,\n not at call sites, so no caller can accidentally claim confidence on an\n unpriced model.\n\n Burst guard: HIGH also requires the calls to span LIVE_TRACE_MIN_ACTIVE_DAYS\n distinct days — a single-day burst clears the call floor with zero evidence\n about a month. Such traffic is demoted to MEDIUM (live data is at least as\n good as a sandbox sweep). `live_active_days=None` means the caller doesn't\n know (direct/legacy callers); both production paths pass the measured value.\n \"\"\"\n if not model_recognized:\n return \"low\"\n if live_trace_count_7d >= LIVE_TRACE_MIN_CALLS:\n if live_active_days is not None and live_active_days < LIVE_TRACE_MIN_ACTIVE_DAYS:\n return \"medium\"\n return \"high\"\n if sandbox_traces and len(sandbox_traces) > 0:\n return \"medium\"\n return \"low\"\n\n\n# ── Live trace ingestion (real production traffic → forecast overrides) ───────\n\ndef _common_prefix_len(a: str, b: str) -> int:\n n = min(len(a), len(b))\n i = 0\n while i < n and a[i] == b[i]:\n i += 1\n return i\n\n\n# Bedrock-style dotted owner prefixes (us.anthropic.claude-..., anthropic.claude-...)\n_VENDOR_DOT_PREFIXES = (\n \"us.\", \"eu.\", \"apac.\",\n \"anthropic.\", \"amazon.\", \"meta.\", \"mistral.\", \"cohere.\", \"ai21.\",\n)\n# Path-style provider namespaces (meta-llama/Llama-..., models/gemini-..., etc.)\n_VENDOR_PATH_PREFIXES = (\n \"accounts/fireworks/models/\", \"bedrock/\", \"vertex_ai/\", \"vertex/\",\n \"openai/\", \"google/\", \"models/\", \"meta-llama/\", \"mistralai/\",\n \"together_ai/\", \"togethercomputer/\", \"groq/\", \"deepseek-ai/\",\n \"cohere/\", \"fireworks/\", \"xai/\", \"anthropic/\",\n)\n\n\ndef _normalize_model_id(model_str: str) -> str:\n \"\"\"Strip provider namespaces and normalize separators on a raw model id so\n real-world ids resolve against the catalog.\n\n Examples:\n us.anthropic.claude-3-5-sonnet-20241022 -> claude-3-5-sonnet-20241022\n meta-llama/Llama-3.1-70B-Instruct -> llama-3-1-70b-instruct\n models/gemini-1.5-pro -> gemini-1-5-pro\n \"\"\"\n ms = model_str.lower().strip()\n changed = True\n while changed: # peel stacked dotted prefixes (us.anthropic.)\n changed = False\n for p in _VENDOR_DOT_PREFIXES:\n if ms.startswith(p):\n ms = ms[len(p):]\n changed = True\n for p in sorted(_VENDOR_PATH_PREFIXES, key=len, reverse=True):\n if ms.startswith(p):\n ms = ms[len(p):]\n break\n if \"/\" in ms:\n ms = ms.rsplit(\"/\", 1)[-1]\n # Version separators vary by vendor: \"3.1\" / \"3_1\" → \"3-1\".\n return ms.replace(\".\", \"-\").replace(\"_\", \"-\")\n\n\ndef _resolve_model_pricing(model_str: Optional[str], defaults: dict) -> tuple[str, str]:\n \"\"\"Map a raw provider model id (e.g. 'claude-sonnet-4-5-20250929',\n 'gpt-4o-2024-08-06', 'meta-llama/Llama-3.1-70B') to (catalog_key, match_kind).\n\n match_kind records how sure we are the catalog price is the REAL price:\n \"exact\" — the id (raw or normalized) is a catalog key\n \"prefix\" — a catalog key prefixes the id: a dated snapshot of a priced\n model (claude-sonnet-4-5-20250929 → claude-sonnet-4-5)\n \"family\" — only a family-level overlap (>=4 shared leading chars): the\n price is a GUESS from a related model, not a verified rate\n (an unpriced claude-fable-9 would bill at another claude row)\n \"default\" — no meaningful overlap; key is the catalog default model\n \"\"\"\n models = defaults.get(\"models\", {})\n default = defaults.get(\"default_model\")\n if not model_str:\n return default, \"default\"\n raw = model_str.lower()\n if raw in models:\n return raw, \"exact\"\n ms = _normalize_model_id(model_str)\n if ms in models:\n return ms, \"exact\"\n # Prefer a key that is an actual prefix of the id (longest such key wins so\n # \"gpt-4o-mini\" beats \"gpt-4o\", \"llama-3-1-70b\" beats nothing shorter).\n prefix_match = max((k for k in models if ms.startswith(k)), key=len, default=None)\n if prefix_match:\n return prefix_match, \"prefix\"\n # Version drift / partial id — fall back to longest common character prefix.\n best_key, best_len = None, 0\n for key in models:\n overlap = _common_prefix_len(key, ms)\n if overlap > best_len:\n best_key, best_len = key, overlap\n # Require a family-level overlap to avoid matching unrelated providers.\n if best_len >= 4:\n return best_key, \"family\"\n return default, \"default\"\n\n\ndef _resolve_model_key(model_str: Optional[str], defaults: dict) -> str:\n \"\"\"Closest catalog pricing key for a raw model id (see _resolve_model_pricing).\"\"\"\n return _resolve_model_pricing(model_str, defaults)[0]\n\n\ndef _model_recognized(model_str: Optional[str], defaults: dict) -> bool:\n \"\"\"True only when the id's price is VERIFIED — an exact catalog hit or a\n dated snapshot of a priced model (prefix match).\n\n A family-level guess no longer counts as recognized: an unpriced model\n billed at a related row's rates is still a guessed price, and a guessed\n price must never carry the tight live-tier band (it used to — claude-fable-5\n rode claude-opus-4-8 rates at HALF the real price with full confidence).\n Family matches still PRICE at the guess (closer than the default model);\n they just cap confidence at LOW and are disclosed via coverage.modelMatch.\n \"\"\"\n if not model_str:\n return False\n return _resolve_model_pricing(model_str, defaults)[1] in (\"exact\", \"prefix\")\n\n\ndef _extract_usage(detail: dict) -> Optional[tuple[int, int, int, int]]:\n \"\"\"Pull (total_input_tokens, cache_read_tokens, cache_creation_tokens,\n output_tokens) from a captured LLM call's stored provider response.\n\n cache_creation is billed at a WRITE premium (Anthropic ~1.25x input); only\n Anthropic reports it separately, so it is 0 for other providers.\n\n Handles the major provider shapes:\n - Anthropic: usage.{input_tokens, output_tokens, cache_read_input_tokens,\n cache_creation_input_tokens}. Anthropic's input_tokens EXCLUDES cached\n tokens, so total input = input + cache_read + cache_creation.\n - OpenAI (+ OpenAI-compatible: DeepSeek, xAI, Together, Groq, Mistral):\n usage.{prompt_tokens, completion_tokens,\n prompt_tokens_details.cached_tokens}. prompt_tokens INCLUDES cached.\n - Gemini: usageMetadata.{promptTokenCount, candidatesTokenCount,\n cachedContentTokenCount}. promptTokenCount INCLUDES cached.\n\n Returns None when the response has no usable usage block.\n \"\"\"\n response = detail.get(\"response\")\n if not isinstance(response, dict):\n return None\n usage = response.get(\"usage\")\n if not isinstance(usage, dict):\n # Gemini's native shape puts counts under usageMetadata.\n usage = response.get(\"usageMetadata\")\n if not isinstance(usage, dict):\n return None\n\n if \"input_tokens\" in usage: # Anthropic\n base_in = int(usage.get(\"input_tokens\") or 0)\n cache_read = int(usage.get(\"cache_read_input_tokens\") or 0)\n cache_creation = int(usage.get(\"cache_creation_input_tokens\") or 0)\n total_in = base_in + cache_read + cache_creation\n out = int(usage.get(\"output_tokens\") or 0)\n cached = cache_read\n elif \"prompt_tokens\" in usage: # OpenAI + OpenAI-compatible\n total_in = int(usage.get(\"prompt_tokens\") or 0)\n out = int(usage.get(\"completion_tokens\") or 0)\n details = usage.get(\"prompt_tokens_details\") or {}\n cached = int(details.get(\"cached_tokens\") or 0) if isinstance(details, dict) else 0\n cache_creation = 0\n elif \"promptTokenCount\" in usage: # Gemini\n total_in = int(usage.get(\"promptTokenCount\") or 0)\n out = int(usage.get(\"candidatesTokenCount\") or 0)\n cached = int(usage.get(\"cachedContentTokenCount\") or 0)\n cache_creation = 0\n else:\n return None\n\n if total_in <= 0 and out <= 0:\n return None\n cached = min(cached, total_in)\n return total_in, cached, min(cache_creation, max(0, total_in - cached)), out\n\n\ndef _parse_iso(ts: Any):\n if not ts:\n return None\n try:\n from datetime import datetime\n return datetime.fromisoformat(str(ts).replace(\"Z\", \"+00:00\").split(\"+\")[0])\n except (ValueError, TypeError):\n return None\n\n\ndef _row_get(row: Any, key: str) -> Any:\n \"\"\"Read a column from a DB row dict (or any mapping).\"\"\"\n try:\n return row[key]\n except (KeyError, IndexError, TypeError):\n if isinstance(row, dict):\n return row.get(key)\n return None\n\n\ndef compute_live_rolling_averages(audit_rows: list) -> dict:\n \"\"\"Per-agent rolling averages from captured LLM calls.\n\n `audit_rows` are LLM_CALL audit entries (row dicts) exposing\n `detail` (JSON string) and `timestamp` (ISO). Returns override keys ready to\n merge into `forecast_spend`: {llm_calls_per_day, cache_hit, input_tokens,\n output_tokens, llm_cost_per_call}. `llm_cost_per_call` is the mean of each\n call's REAL $ (its own model + tokens + cache) — so the high tier prices the\n actual model mix, not a single reconstructed model. Returns {} when no rows\n carry usable token usage.\n \"\"\"\n defaults = load_defaults()\n usages: list[tuple[int, int, int]] = []\n per_call_costs: list[float] = []\n times = []\n for row in audit_rows:\n raw = _row_get(row, \"detail\")\n if not raw:\n continue\n try:\n detail = json.loads(raw)\n except (json.JSONDecodeError, TypeError):\n continue\n u = _extract_usage(detail)\n if u is None:\n continue\n usages.append(u)\n # Real per-call $ using THIS call's own model (blends a mixed-model fleet).\n _tin, _cached, _cc, _out = u\n _mk = _resolve_model_key(detail.get(\"model\"), defaults)\n per_call_costs.append(_call_cost_usd(_tin, _cached, _out, _mk, defaults, cache_creation=_cc))\n t = _parse_iso(_row_get(row, \"timestamp\"))\n if t is not None:\n times.append(t)\n\n if not usages:\n return {}\n\n n = len(usages)\n total_in = sum(x[0] for x in usages)\n total_cached = sum(x[1] for x in usages)\n total_out = sum(x[3] for x in usages)\n\n # Observed call rate over the actual span the data covers, clamped to the\n # 7-day window we queried (avoids understating a 2-day-old agent and\n # overstating a single burst).\n if len(times) >= 2:\n span_days = (max(times) - min(times)).total_seconds() / 86400.0\n span_days = min(7.0, max(1.0, span_days))\n else:\n span_days = 1.0\n\n return {\n # Audit rows are individual LLM calls → this is total LLM calls/day\n # (= runs × turns). forecast_spend uses it directly at the live tier and\n # does NOT re-multiply by turns_per_run.\n \"llm_calls_per_day\": max(1, round(n / span_days)),\n \"cache_hit\": round(total_cached / total_in * 100) if total_in > 0 else 0,\n \"input_tokens\": round(total_in / n),\n \"output_tokens\": round(total_out / n),\n \"llm_cost_per_call\": round(sum(per_call_costs) / n, 6) if per_call_costs else None,\n # Honesty metadata, not a forecast input: how many days of traffic the\n # averages above actually cover. A 20-minute burst clamps to 1 day —\n # the UI captions the high-tier number with this so ±15% never rides\n # on an unrepresentative window.\n \"observed_days\": round(span_days, 1),\n # Distinct calendar days with traffic — the burst-guard signal. Unlike\n # the clamped span above, this can't be inflated by two calls far apart\n # in one day. None when rows carry no timestamps (guard stays out).\n \"active_days\": len({t.date() for t in times}) if times else None,\n }\n\n\ndef compute_sandbox_averages(traces: list) -> dict:\n \"\"\"Per-run turn count + per-turn token averages from sandbox traces that\n captured real usage (`turn_usage`). Mirrors `compute_live_rolling_averages`\n but for the medium tier. Returns {turns_per_run, input_tokens, output_tokens},\n or {} when no trace carries usable turn usage.\n\n Divisor note: turns_per_run is averaged over RUNS (one trace = one run);\n tokens are averaged over TURNS (the per-LLM-call basis the cost math uses).\n\n Deliberately NO cache_hit: simulations run each scenario cold, so their\n measured cache rate is structurally 0% — treating that as a measurement of\n production cache locality doubled a cache-heavy agent's forecast while\n CLAIMING higher confidence. Cache stays declared/default until live traces\n (which do see real locality) measure it.\n \"\"\"\n per_run_turns: list[int] = []\n in_sum = out_sum = turn_n = 0\n for t in traces or []:\n tu = (t.get(\"turn_usage\") if isinstance(t, dict) else None) or []\n usable = [x for x in tu if isinstance(x, dict)\n and (int(x.get(\"input_tokens\") or 0) + int(x.get(\"output_tokens\") or 0)) > 0]\n if not usable:\n continue\n per_run_turns.append(len(usable))\n for x in usable:\n in_sum += int(x.get(\"input_tokens\") or 0)\n out_sum += int(x.get(\"output_tokens\") or 0)\n turn_n += 1\n if not per_run_turns or turn_n == 0:\n return {}\n return {\n \"turns_per_run\": max(1, round(sum(per_run_turns) / len(per_run_turns))),\n \"input_tokens\": round(in_sum / turn_n),\n \"output_tokens\": round(out_sum / turn_n),\n }\n\n\ndef compute_spend_timeseries(\n audit_rows: list,\n *,\n days: int = 30,\n defaults: Optional[dict] = None,\n) -> list[dict]:\n \"\"\"Observed daily LLM spend from captured calls, one row per calendar day.\n\n Returns `days` rows ending today (UTC), each {date, usd, calls}. Days with\n no traffic are included with zeros so the chart x-axis stays continuous.\n Cost is the measured LLM token cost only (tools/infra aren't in LLM_CALL\n capture); the caller labels it as such.\n \"\"\"\n from datetime import datetime, timedelta\n\n defaults = defaults or load_defaults()\n buckets: dict[str, list[float]] = {} # date -> [usd, calls]\n\n for row in audit_rows:\n raw = _row_get(row, \"detail\")\n ts = _row_get(row, \"timestamp\")\n if not raw or not ts:\n continue\n try:\n detail = json.loads(raw)\n except (json.JSONDecodeError, TypeError):\n continue\n u = _extract_usage(detail)\n if u is None:\n continue\n total_in, cached, cache_creation, out = u\n model_key = _resolve_model_key(detail.get(\"model\"), defaults)\n usd = _call_cost_usd(total_in, cached, out, model_key, defaults, cache_creation=cache_creation)\n date = str(ts)[:10]\n b = buckets.setdefault(date, [0.0, 0.0])\n b[0] += usd\n b[1] += 1\n\n today = datetime.utcnow().date()\n series: list[dict] = []\n for i in range(days - 1, -1, -1):\n d = (today - timedelta(days=i)).isoformat()\n usd, calls = buckets.get(d, [0.0, 0.0])\n series.append({\"date\": d, \"usd\": round(usd, 2), \"calls\": int(calls)})\n return series\n\n\ndef compute_month_to_date_spend(\n audit_rows: list, *, now=None, defaults: Optional[dict] = None\n) -> float:\n \"\"\"Observed LLM spend so far this calendar month, from captured calls.\n\n Real money spent (not forecast), summed from `response.usage` × model\n pricing — the basis for the budget cap alert. Caller passes rows already\n filtered to the agent; this filters to the current month and sums.\n \"\"\"\n from datetime import datetime as _dt\n\n defaults = defaults or load_defaults()\n now = now or _dt.utcnow()\n month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)\n\n total = 0.0\n for row in audit_rows:\n raw = _row_get(row, \"detail\")\n ts = _parse_iso(_row_get(row, \"timestamp\"))\n if not raw or ts is None or ts < month_start:\n continue\n try:\n detail = json.loads(raw)\n except (json.JSONDecodeError, TypeError):\n continue\n u = _extract_usage(detail)\n if u is None:\n continue\n total_in, cached, cache_creation, out = u\n model_key = _resolve_model_key(detail.get(\"model\"), defaults)\n total += _call_cost_usd(total_in, cached, out, model_key, defaults, cache_creation=cache_creation)\n return round(total, 2)\n\n\n# Anthropic bills a cache WRITE (cache_creation) at ~1.25x the input rate; a\n# cache READ gets the discount. Billing writes at 1.0x under-counted cache-heavy\n# agents on the observed (high-confidence) path.\nCACHE_WRITE_MULTIPLIER = 1.25\n\n\ndef _call_cost_usd(\n total_input: int, cached_input: int, output: int, model_key: str, defaults: dict,\n cache_creation: int = 0,\n) -> float:\n \"\"\"Dollar cost of a single captured call: cache reads get the discount, cache\n writes get the write premium, the rest is billed at the input rate.\"\"\"\n models = defaults.get(\"models\", {})\n mp = models.get(model_key) or models.get(defaults.get(\"default_model\")) or {}\n in_price = float(mp.get(\"input_per_mtok\", 0.0)) / 1_000_000.0\n out_price = float(mp.get(\"output_per_mtok\", 0.0)) / 1_000_000.0\n cache_discount = float(mp.get(\"cache_discount\", 0.5))\n non_cached = max(0, total_input - cached_input - cache_creation)\n return (\n non_cached * in_price\n + cached_input * in_price * (1.0 - cache_discount)\n + cache_creation * in_price * CACHE_WRITE_MULTIPLIER\n + output * out_price\n )\n\n\n# ── Spend anomaly detection (last 24h vs trailing baseline) ──────────────────\n\n# Fire when the last 24h cost this many times the trailing daily average.\nSPEND_ANOMALY_RATIO = 3.0\n# Baseline gates: a brand-new agent (or one doing pennies of traffic) has no\n# meaningful \"usual rate\" — never flag those, just report insufficient baseline.\nSPEND_ANOMALY_MIN_BASELINE_CALLS = 10\nSPEND_ANOMALY_MIN_BASELINE_DAILY_USD = 0.05\n\n\ndef detect_spend_anomaly(\n audit_rows: list,\n *,\n now=None,\n defaults: Optional[dict] = None,\n) -> dict:\n \"\"\"Compare the last 24h of observed LLM spend against the trailing 7-day\n daily average (excluding the last 24h) and flag a >=SPEND_ANOMALY_RATIO\n jump.\n\n `audit_rows` are LLM_CALL audit entries (row dicts) covering the\n last 8 days for one agent. Returns:\n\n {\"flagged\": bool, \"ratio\": float, \"last24hUsd\": float,\n \"baselineDailyUsd\": float, \"last24hCalls\": int,\n \"baselineDailyCalls\": float, \"drivers\": [str, ...],\n \"baselineSufficient\": bool}\n\n Drivers are plain-English fragments (\"call volume is 4x its usual rate\")\n so the Slack alert and the CFO-facing banner can use them verbatim.\n \"\"\"\n from datetime import datetime, timedelta\n\n defaults = defaults or load_defaults()\n now = now or datetime.utcnow()\n cutoff_recent = now - timedelta(days=1)\n cutoff_baseline = now - timedelta(days=8)\n\n # (usd, total_tokens, model_key) per call, split into the two windows.\n recent: list[tuple[float, int, str]] = []\n baseline: list[tuple[float, int, str]] = []\n for row in audit_rows:\n raw = _row_get(row, \"detail\")\n ts = _parse_iso(_row_get(row, \"timestamp\"))\n if not raw or ts is None or ts < cutoff_baseline:\n continue\n try:\n detail = json.loads(raw)\n except (json.JSONDecodeError, TypeError):\n continue\n u = _extract_usage(detail)\n if u is None:\n continue\n total_in, cached, cache_creation, out = u\n model_key = _resolve_model_key(detail.get(\"model\"), defaults)\n usd = _call_cost_usd(total_in, cached, out, model_key, defaults, cache_creation=cache_creation)\n call = (usd, total_in + out, model_key)\n (recent if ts >= cutoff_recent else baseline).append(call)\n\n baseline_usd = sum(c[0] for c in baseline)\n baseline_daily_usd = baseline_usd / 7.0\n baseline_daily_calls = len(baseline) / 7.0\n recent_usd = sum(c[0] for c in recent)\n\n result = {\n \"flagged\": False,\n \"ratio\": 0.0,\n \"last24hUsd\": round(recent_usd, 2),\n \"baselineDailyUsd\": round(baseline_daily_usd, 2),\n \"last24hCalls\": len(recent),\n \"baselineDailyCalls\": round(baseline_daily_calls, 1),\n \"drivers\": [],\n \"baselineSufficient\": (\n len(baseline) >= SPEND_ANOMALY_MIN_BASELINE_CALLS\n and baseline_daily_usd >= SPEND_ANOMALY_MIN_BASELINE_DAILY_USD\n ),\n }\n if not result[\"baselineSufficient\"]:\n return result\n\n ratio = recent_usd / baseline_daily_usd\n result[\"ratio\"] = round(ratio, 1)\n if ratio < SPEND_ANOMALY_RATIO:\n return result\n result[\"flagged\"] = True\n\n # ── Drivers: what changed vs the baseline window ──────────────────────────\n drivers: list[str] = []\n\n # 1. Call volume\n if baseline_daily_calls > 0:\n call_ratio = len(recent) / baseline_daily_calls\n if call_ratio >= 2.0:\n drivers.append(\n f\"call volume is {call_ratio:.1f}x its usual daily rate \"\n f\"({len(recent)} calls vs ~{baseline_daily_calls:.0f}/day)\"\n )\n\n # 2. Call size (tokens per call)\n if recent and baseline:\n recent_tok = sum(c[1] for c in recent) / len(recent)\n base_tok = sum(c[1] for c in baseline) / len(baseline)\n if base_tok > 0 and recent_tok / base_tok >= 1.5:\n drivers.append(\n f\"individual calls are {recent_tok / base_tok:.1f}x larger \"\n f\"than usual ({recent_tok:.0f} vs {base_tok:.0f} tokens per call)\"\n )\n\n # 3. Model mix — a model that wasn't in the baseline now driving spend\n base_models = {c[2] for c in baseline}\n new_model_usd: dict[str, float] = {}\n for usd, _tok, mk in recent:\n if mk not in base_models:\n new_model_usd[mk] = new_model_usd.get(mk, 0.0) + usd\n if new_model_usd and recent_usd > 0:\n top_model = max(new_model_usd, key=new_model_usd.get)\n share = new_model_usd[top_model] / recent_usd * 100\n if share >= 25:\n drivers.append(\n f\"a model not seen before ({top_model}) accounts for \"\n f\"{share:.0f}% of the last 24h spend\"\n )\n\n if not drivers:\n # Spend jumped without volume/size/model shifts standing out — say so\n # rather than leaving the alert unexplained.\n drivers.append(\"no single driver stands out; spend rose across the board\")\n result[\"drivers\"] = drivers\n return result\n\n\n# ── Token estimate (static, from capability tree alone) ──────────────────────\n\ndef _estimate_tokens_per_call(agent_config: dict, defaults: dict, model_inflation: float, turns_per_run: float = 1.0) -> tuple[int, int]:\n \"\"\"Estimate input + output tokens per call from capability tree alone.\n\n Used at the low confidence tier. Sandbox/live traces would override this.\n\n A declared `avg_context_tokens` (a RAG agent's retrieved context, or a long\n system prompt the extractor recovered) replaces the flat system-prompt\n assumption — without it, a 1-tool agent carrying an 80k-token context is\n priced identically to a 1-tool agent with a one-line prompt.\n\n `turns_per_run` adds an averaged history-growth term: later turns in an\n agentic loop carry the accumulated conversation, so the AVERAGE turn's input\n is larger than the first turn's. (Per-turn basis — the volume layer scales\n by total turns separately.)\n \"\"\"\n td = defaults[\"turn_defaults\"]\n num_tools = len(agent_config.get(\"tools\", []))\n\n declared_ctx = agent_config.get(\"avg_context_tokens\")\n try:\n declared_ctx = int(declared_ctx) if declared_ctx else 0\n except (TypeError, ValueError):\n declared_ctx = 0\n base_context = declared_ctx if declared_ctx > 0 else td[\"system_prompt_tokens\"]\n\n # Average accumulated history across the run (linear growth → mean is half\n # the final). 0 for a single-turn agent.\n history_growth = float(td.get(\"history_growth_per_turn\", 0))\n avg_history = history_growth * max(0.0, float(turns_per_run) - 1.0) / 2.0\n\n in_tokens = (\n base_context\n + num_tools * td[\"tool_overhead_per_tool\"]\n + td[\"user_message_tokens\"]\n + td[\"tool_result_tokens\"] # assume at least one tool result returned per call\n + avg_history\n )\n out_tokens = td[\"completion_tokens\"]\n return int(in_tokens * model_inflation), int(out_tokens * model_inflation)\n\n\n# ── Tool pricing resolution (aliases + known-free) ──────────────────────────\n\n# Real tool names → the pricing key that represents their per-call API cost.\n# Without this, `gmail`, `aws`, etc. miss the `tool_action_costs` table entirely\n# and price at $0 while also counting as \"uncovered\" in the confidence panel.\nTOOL_PRICING_ALIASES: dict[str, str] = {\n # Email providers bill per message like SendGrid.\n \"gmail\": \"sendgrid\", \"email\": \"sendgrid\", \"ses\": \"sendgrid\",\n \"mailgun\": \"sendgrid\", \"mail\": \"sendgrid\", \"smtp\": \"sendgrid\",\n \"postmark\": \"sendgrid\", \"sendgrid_mail\": \"sendgrid\",\n # SMS / voice.\n \"sms\": \"twilio\", \"messagebird\": \"twilio\", \"vonage\": \"twilio\",\n # AWS — generic handle and per-service aliases.\n \"aws\": \"aws_lambda\", \"lambda\": \"aws_lambda\", \"aws_lambda\": \"aws_lambda\",\n \"s3\": \"aws_s3\", \"aws_s3\": \"aws_s3\",\n \"rds\": \"aws_rds\", \"aws_rds\": \"aws_rds\",\n # Vector stores / embeddings.\n \"vector_db\": \"pinecone\", \"pinecone\": \"pinecone\",\n \"embeddings\": \"openai_embeddings\",\n}\n\n# Tools whose APIs are effectively free at agent volumes. Priced at $0 (correct)\n# but counted as COVERED — a known zero, not an unknown gap.\nKNOWN_FREE_TOOLS: frozenset[str] = frozenset({\n \"slack\", \"github\", \"zendesk\", \"salesforce\", \"pagerduty\", \"hubspot\",\n \"calendly\", \"jira\", \"datadog\", \"okta\", \"google_sheets\", \"google_workspace\",\n \"notion\", \"linear\", \"discord\", \"teams\", \"asana\", \"trello\", \"intercom\",\n \"freshdesk\", \"servicenow\", \"confluence\",\n})\n\n_TOOL_READ_PREFIXES = (\n \"get_\", \"list_\", \"read_\", \"search_\", \"query_\", \"check_\",\n \"describe_\", \"fetch_\", \"lookup_\", \"find_\", \"show_\",\n)\n\n\ndef _resolve_pricing_key(tool_name: str) -> str:\n \"\"\"Map a real tool name to its pricing-table key (alias applied).\"\"\"\n t = (tool_name or \"\").lower()\n return TOOL_PRICING_ALIASES.get(t, t)\n\n\ndef _tool_is_priced(tac: dict, tool_name: str) -> bool:\n \"\"\"True if we have a defensible per-call cost for this tool — either a\n pricing-table entry (directly or via alias) or a known-free API ($0).\"\"\"\n t = (tool_name or \"\").lower()\n if t in KNOWN_FREE_TOOLS:\n return True\n return _resolve_pricing_key(t) in tac\n\n\ndef _tool_action_price(tac: dict, tool_name: str, action_name: str) -> float:\n \"\"\"Per-call USD cost for one tool.action, resolving aliases.\n\n Exact action match wins. For an *aliased* tool whose action is named\n differently from the canonical provider (e.g. gmail.send_message →\n sendgrid), a billable (non-read) action falls back to the cheapest known\n price of that provider as a floor. Direct-match tools keep exact-or-zero\n so already-correct pricing is unchanged.\n \"\"\"\n raw = (tool_name or \"\").lower()\n if raw in KNOWN_FREE_TOOLS:\n return 0.0\n key = _resolve_pricing_key(raw)\n pricing = tac.get(key)\n if not pricing:\n return 0.0\n if action_name in pricing:\n return float(pricing[action_name])\n if key != raw and not action_name.lower().startswith(_TOOL_READ_PREFIXES):\n nonzero = [float(v) for v in pricing.values() if float(v) > 0]\n if nonzero:\n return min(nonzero)\n return 0.0\n\n\n# ── Tool cost per call (uniform action distribution baseline) ───────────────\n\ndef _estimate_tool_cost_per_call(\n agent_config: dict, defaults: dict, sandbox_traces: Optional[list] = None\n) -> tuple[float, list[dict], bool]:\n \"\"\"Expected per-call tool cost + per-action (tool, action, cost, weight) rows.\n\n `weight` is each action's per-call probability. With sandbox traces present\n it's the REAL observed frequency (count/total observed steps); otherwise it's\n a uniform 1/N guess. Returns (cost_per_call, rows, mix_measured).\"\"\"\n tac = defaults[\"tool_action_costs\"]\n base: list[tuple[str, str, float]] = []\n for tool in agent_config.get(\"tools\", []):\n tool_name = (tool.get(\"name\") or \"\").lower()\n for action_obj in tool.get(\"actions\", []):\n if isinstance(action_obj, dict):\n action_name = action_obj.get(\"action\") or action_obj.get(\"name\") or \"\"\n else:\n action_name = str(action_obj)\n if not action_name:\n continue\n base.append((tool_name, action_name, _tool_action_price(tac, tool_name, action_name)))\n\n if not base:\n return 0.0, [], False\n\n # Observed weighting when traces exist: weight each action by its real share.\n weights: dict[str, float] = {}\n mix_measured = False\n if sandbox_traces:\n counts = {f\"{tn}.{an}\": _count_action_occurrences(sandbox_traces, {f\"{tn}.{an}\"}) for tn, an, _ in base}\n total_obs = sum(counts.values())\n if total_obs > 0:\n mix_measured = True\n weights = {k: c / total_obs for k, c in counts.items()}\n if not mix_measured:\n # Uniform fallback: each action equally likely per call.\n n = len(base)\n weights = {f\"{tn}.{an}\": 1.0 / n for tn, an, _ in base}\n\n rows = [(tn, an, cost, weights.get(f\"{tn}.{an}\", 0.0)) for tn, an, cost in base]\n total_per_call = sum(cost * w for _, _, cost, w in rows)\n return total_per_call, rows, mix_measured\n\n\n# ── Per-agent sensitivity (±20% perturbation) ────────────────────────────────\n\n_SENSITIVITY_CACHE: dict[tuple[str, str, str], list[dict]] = {}\n\n\n# ── Data sources panel ───────────────────────────────────────────────────────\n\ndef compute_data_sources(\n *,\n sandbox_sim_count: int,\n live_call_count_7d: int,\n agent_config: dict,\n defaults: Optional[dict] = None,\n oldest_snapshot_days: Optional[int] = None,\n snapshot_count: int = 0,\n) -> list[dict]:\n \"\"\"Build the Confidence sources panel rows from real counts.\n\n Each row is {label, status, statusTone}; statusTone is one of\n \"calibrated\" / \"active\" / \"partial\" / \"disconnected\" so the frontend\n can apply the existing tone color map.\n \"\"\"\n defaults = defaults or load_defaults()\n tac = defaults.get(\"tool_action_costs\", {}) or {}\n\n # Sandbox\n if sandbox_sim_count >= 10:\n sb_status, sb_tone = f\"{sandbox_sim_count} RUNS\", \"calibrated\"\n elif sandbox_sim_count > 0:\n sb_status, sb_tone = f\"{sandbox_sim_count} RUN\" + (\"S\" if sandbox_sim_count > 1 else \"\"), \"partial\"\n else:\n sb_status, sb_tone = \"NO RUNS YET\", \"disconnected\"\n\n # Live LLM call capture\n if live_call_count_7d > 0:\n live_status, live_tone = f\"{live_call_count_7d} CALLS\", \"active\"\n else:\n live_status, live_tone = \"NOT CONNECTED\", \"disconnected\"\n\n # Tool API pricing — fraction of agent's tools with a known per-call cost\n # (priced table entry, alias, or a known-free $0 API).\n tool_names = [(t.get(\"name\") or \"\").lower() for t in agent_config.get(\"tools\", []) if t.get(\"name\")]\n total_tools = len(tool_names)\n covered_tools = sum(1 for n in tool_names if _tool_is_priced(tac, n))\n if total_tools == 0:\n pricing_status, pricing_tone = \"NO TOOLS\", \"disconnected\"\n elif covered_tools == total_tools:\n pricing_status, pricing_tone = f\"{covered_tools} OF {total_tools} TOOLS\", \"calibrated\"\n elif covered_tools > 0:\n pricing_status, pricing_tone = f\"{covered_tools} OF {total_tools} TOOLS\", \"partial\"\n else:\n pricing_status, pricing_tone = f\"0 OF {total_tools} TOOLS\", \"disconnected\"\n\n # Historical baseline\n if snapshot_count == 0:\n hist_status, hist_tone = \"NOT CONNECTED\", \"disconnected\"\n elif oldest_snapshot_days is not None and oldest_snapshot_days >= 30:\n hist_status, hist_tone = f\"{oldest_snapshot_days}D HISTORY\", \"active\"\n else:\n days = oldest_snapshot_days if oldest_snapshot_days is not None else 0\n hist_status, hist_tone = f\"NEEDS 30D · {days}D SO FAR\", \"partial\"\n\n return [\n {\"label\": \"Sandbox simulation traces\", \"status\": sb_status, \"statusTone\": sb_tone},\n {\"label\": \"Live LLM call capture · last 7 days\", \"status\": live_status, \"statusTone\": live_tone},\n {\"label\": \"Tool API pricing\", \"status\": pricing_status, \"statusTone\": pricing_tone},\n {\"label\": \"Historical baseline\", \"status\": hist_status, \"statusTone\": hist_tone},\n ]\n\n\ndef _stable_hash(payload: Any) -> str:\n return hashlib.sha1(\n json.dumps(payload, sort_keys=True, default=str).encode()\n ).hexdigest()[:16]\n\n\ndef _color_for_rank(rank: int, total: int) -> str:\n if rank == 0:\n return \"linear-gradient(90deg, #fecaca, #ef4444)\"\n if rank <= max(1, total // 2):\n return \"linear-gradient(90deg, #fde68a, #f59e0b)\"\n return \"linear-gradient(90deg, #d1d5db, #9ca3af)\"\n\n\ndef _compute_per_agent_sensitivity(\n agent_config: dict,\n sandbox_traces: Optional[list],\n live_trace_count_7d: int,\n overrides: dict,\n baseline_point: float,\n defaults: dict,\n org_id: Optional[str] = None,\n) -> list[dict]:\n \"\"\"Per-agent sensitivity ranking via ±20% perturbation.\n\n Re-runs the forecast 5x (one per input) at +20% and -20% of baseline,\n averages the |Δpoint|/baseline ratio, ranks by impact. Model choice is\n measured as the spread across all available models.\n \"\"\"\n if baseline_point <= 0:\n return [] # can't perturb a zero baseline — return nothing, never canned %\n\n sd = defaults[\"scenario_defaults\"]\n # Perturb the SAME volume key the baseline forecast is driven by. At the\n # high (live) tier, volume is observed `llm_calls_per_day`; perturbing\n # `runs_per_day` there flips the forecast onto a runs×turns basis (a totally\n # different volume), which produced absurd swings (e.g. 2000%). On the live\n # basis we perturb `llm_calls_per_day`; otherwise `runs_per_day`.\n _live_calls = overrides.get(\"llm_calls_per_day\")\n _on_live_basis = (\n _live_calls is not None\n and overrides.get(\"runs_per_day\") is None\n and overrides.get(\"calls_per_day\") is None\n and overrides.get(\"turns_per_run\") is None\n )\n if _on_live_basis:\n base_calls = float(_live_calls)\n _volume_key = \"llm_calls_per_day\"\n else:\n base_calls = float(_first_set(\n overrides.get(\"runs_per_day\"),\n overrides.get(\"calls_per_day\"),\n agent_config.get(\"expected_calls_per_day\"),\n default=sd[\"default_calls_per_day\"],\n ))\n _volume_key = \"runs_per_day\"\n base_runtime = float(overrides.get(\"runtime\") or sd[\"default_runtime_seconds\"])\n base_cache = float(overrides.get(\"cache_hit\", sd[\"default_cache_hit_rate\"] * 100))\n base_retry = float(overrides.get(\"retry_rate\", sd[\"default_retry_rate\"] * 100))\n # Mirror forecast_spend's model resolution so the model-choice spread is\n # measured against the agent's actual baseline model, not always the default.\n _declared = (agent_config.get(\"simulation_model\") or \"\").strip()\n base_model = overrides.get(\"model\")\n if not base_model and _declared:\n _base_key, _base_match = _resolve_model_pricing(_declared, defaults)\n if _base_match != \"default\": # mirror forecast_spend: family guess still prices\n base_model = _base_key\n if not base_model:\n base_model = defaults[\"default_model\"]\n\n def _point_with(**override_updates) -> float:\n merged = {**overrides, **override_updates}\n result = forecast_spend(\n agent_config,\n sandbox_traces=sandbox_traces,\n live_trace_count_7d=live_trace_count_7d,\n overrides=merged,\n org_id=org_id,\n _skip_sensitivity=True,\n )\n return float(result.get(\"pointExact\", result.get(\"point\", 0)))\n\n def _avg_pct_change(plus_overrides: dict, minus_overrides: dict) -> int:\n plus_point = _point_with(**plus_overrides)\n minus_point = _point_with(**minus_overrides)\n delta = (abs(plus_point - baseline_point) + abs(minus_point - baseline_point)) / 2\n return int(round(delta / baseline_point * 100))\n\n sensitivities: list[tuple[str, int]] = []\n\n sensitivities.append((\"Calls per day\", _avg_pct_change(\n {_volume_key: max(1, int(round(base_calls * 1.2)))},\n {_volume_key: max(1, int(round(base_calls * 0.8)))},\n )))\n\n # Model choice — spread across all available models (categorical, no ±20%)\n model_points = [baseline_point]\n for m_name in defaults.get(\"models\", {}).keys():\n if m_name == base_model:\n continue\n try:\n model_points.append(_point_with(model=m_name))\n except Exception:\n continue\n if len(model_points) > 1:\n model_pct = int(round((max(model_points) - min(model_points)) / baseline_point * 100))\n else:\n model_pct = 0\n sensitivities.append((\"Model choice\", model_pct))\n\n sensitivities.append((\"Cache hit rate\", _avg_pct_change(\n {\"cache_hit\": min(100.0, base_cache * 1.2)},\n {\"cache_hit\": max(0.0, base_cache * 0.8)},\n )))\n\n sensitivities.append((\"Runtime / call\", _avg_pct_change(\n {\"runtime\": base_runtime * 1.2},\n {\"runtime\": base_runtime * 0.8},\n )))\n\n sensitivities.append((\"Retry rate\", _avg_pct_change(\n {\"retry_rate\": min(100.0, base_retry * 1.2)},\n {\"retry_rate\": max(0.0, base_retry * 0.8)},\n )))\n\n sensitivities.sort(key=lambda t: t[1], reverse=True)\n\n return [\n {\"label\": label, \"pct\": pct, \"color\": _color_for_rank(i, len(sensitivities))}\n for i, (label, pct) in enumerate(sensitivities)\n ]\n\n\ndef _cached_sensitivity(\n agent_config: dict,\n sandbox_traces: Optional[list],\n live_trace_count_7d: int,\n overrides: dict,\n baseline_point: float,\n defaults: dict,\n org_id: Optional[str] = None,\n) -> list[dict]:\n agent_id = str(agent_config.get(\"id\") or agent_config.get(\"name\") or \"unknown\")\n key = (\n agent_id,\n _stable_hash(agent_config),\n # org_id is in the key because per-org cost overrides shift the\n # perturbation results; clear_override_caches() busts on writes.\n _stable_hash({\"overrides\": overrides, \"org\": org_id, \"tier_inputs\": [bool(sandbox_traces), live_trace_count_7d > 0]}),\n )\n cached = _SENSITIVITY_CACHE.get(key)\n if cached is not None:\n return cached\n result = _compute_per_agent_sensitivity(\n agent_config, sandbox_traces, live_trace_count_7d, overrides, baseline_point, defaults,\n org_id=org_id,\n )\n _SENSITIVITY_CACHE[key] = result\n return result\n\n\n# ── Main forecast function ───────────────────────────────────────────────────\n\ndef _infer_archetype(agent_config: dict) -> str:\n \"\"\"Infer an agent archetype from its tool names (mirrors main._infer_agent_type_from_config,\n plus a scheduler bucket). Used only to pick a cold-start turns-per-run default.\"\"\"\n tool_names = {(t.get(\"name\") or \"\").lower() for t in agent_config.get(\"tools\", [])}\n if tool_names and tool_names <= {\"calendly\", \"calendar\", \"google_calendar\", \"scheduler\", \"cron\"}:\n return \"scheduler\"\n if \"zendesk\" in tool_names or (\"stripe\" in tool_names and (\"email\" in tool_names or \"sendgrid\" in tool_names or \"gmail\" in tool_names)):\n return \"support\"\n if \"github\" in tool_names or \"aws\" in tool_names:\n return \"ops\" if \"pagerduty\" in tool_names else \"devops\"\n if \"hubspot\" in tool_names or \"calendly\" in tool_names or \"salesforce\" in tool_names:\n return \"sales\"\n return \"default\"\n\n\ndef _default_turns_per_run(agent_config: dict, defaults: dict) -> float:\n \"\"\"Archetype-aware cold-start turns/run (a scheduler isn't a devops loop).\n Falls back to the flat avg_turns_per_run when no map / archetype match.\n\n Bounded by capability: archetype comes from tool NAMES, so a one-action\n `github` status checker infers \"devops\" and inherited an 8-turn loop guess.\n An agent with <=2 declared actions has nothing to loop over — cap at 2.\n \"\"\"\n sd = defaults.get(\"scenario_defaults\", {})\n by_arch = defaults.get(\"default_turns_per_run_by_archetype\") or {}\n arch = _infer_archetype(agent_config)\n turns = float(by_arch.get(arch, by_arch.get(\"default\", sd.get(\"avg_turns_per_run\", 4))))\n n_actions = sum(len(t.get(\"actions\") or []) for t in agent_config.get(\"tools\", []))\n if 0 < n_actions <= 2:\n turns = min(turns, 2.0)\n return turns\n\n\ndef forecast_spend(\n agent_config: dict,\n *,\n sandbox_traces: Optional[list] = None,\n live_trace_count_7d: int = 0,\n overrides: Optional[dict] = None,\n previous_snapshot_point: Optional[float] = None,\n org_id: Optional[str] = None,\n _skip_sensitivity: bool = False,\n) -> dict:\n \"\"\"Compute a spend forecast for an agent.\n\n `org_id` applies that org's cost_overrides (negotiated model rates etc.)\n on top of the YAML defaults. Returns a dict matching the shape the\n frontend's MockSpend type expects.\n \"\"\"\n defaults = load_defaults(org_id)\n overrides = overrides or {}\n\n # ── Coverage: do we actually know this agent's model? ──\n # The forecast prices in a known model; if the agent declares one we don't\n # recognize, we still compute a number (at the priced model's rates) but must\n # disclose it and not claim a tight band on a guessed price.\n declared_model = (agent_config.get(\"simulation_model\") or \"\").strip()\n declared_key, declared_match = (\n _resolve_model_pricing(declared_model, defaults) if declared_model else (None, None)\n )\n model_recognized = (not declared_model) or declared_match in (\"exact\", \"prefix\")\n\n # ── Tier (the unrecognized-model cap now lives inside _detect_tier) ──\n _live_active_days = overrides.get(\"active_days\")\n tier = _detect_tier(sandbox_traces, live_trace_count_7d,\n model_recognized=model_recognized,\n live_active_days=_live_active_days)\n # Disclosed alongside the tier so the UI can say WHY a heavy-traffic agent\n # isn't showing ±15% (burst guard demotes single-day bursts to medium).\n burst_capped = (\n model_recognized\n and live_trace_count_7d >= LIVE_TRACE_MIN_CALLS\n and _live_active_days is not None\n and _live_active_days < LIVE_TRACE_MIN_ACTIVE_DAYS\n )\n band = defaults[\"confidence_bands\"][tier]\n\n # ── Resolve inputs (overrides > agent's declared model > YAML default) ──\n # Pricing precedence: an explicit slider/override model wins; otherwise the\n # agent's own declared model is priced at its real rate (a Haiku agent must\n # not be billed as Sonnet); only then fall back to the default. This is what\n # makes persisting simulation_model actually move the number.\n sd = defaults[\"scenario_defaults\"]\n model_name = overrides.get(\"model\")\n # A family-level guess still prices at the guessed row (closer to reality\n # than the catalog default) — the LOW-tier cap + coverage.modelMatch carry\n # the honesty; only a no-overlap id falls through to the default model.\n if not model_name and declared_model and declared_match != \"default\":\n model_name = declared_key\n if not model_name or model_name not in defaults[\"models\"]:\n model_name = defaults[\"default_model\"]\n model_pricing = defaults[\"models\"][model_name]\n inflation = float(model_pricing.get(\"tokenizer_inflation\", 1.0))\n\n # ── Availability: never fabricate a number with no real signal ──\n # Suppress the forecast unless the user has given us SOMETHING real: declared\n # volume, a sandbox sweep, or live traces. A bare freshly-registered agent\n # returns available=False instead of a defaults-only guess dressed as a number.\n has_volume = (\n agent_config.get(\"expected_calls_per_day\") is not None\n or overrides.get(\"runs_per_day\") is not None\n or overrides.get(\"calls_per_day\") is not None\n or overrides.get(\"llm_calls_per_day\") is not None\n )\n available = has_volume or bool(sandbox_traces) or (live_trace_count_7d >= LIVE_TRACE_MIN_CALLS_FORECAST)\n if not available:\n _tn = [(t.get(\"name\") or \"\").lower() for t in agent_config.get(\"tools\", []) if t.get(\"name\")]\n _tc = defaults.get(\"tool_action_costs\", {}) or {}\n return {\n \"available\": False,\n # 1–4 captured calls: capture IS working, we just won't extrapolate\n # a month from it yet — the UI shows progress, not \"no data\".\n \"reason\": \"collecting_live_traffic\" if live_trace_count_7d > 0 else \"no_data\",\n \"liveCalls7d\": live_trace_count_7d,\n \"liveCallsNeeded\": LIVE_TRACE_MIN_CALLS_FORECAST,\n \"needs\": [\"declare_volume\", \"sandbox_sweep\", \"live_traces\"],\n \"point\": None, \"low\": None, \"high\": None, \"annual\": None, \"pointExact\": None,\n \"vsLastMonth\": 0, \"vsLastMonthAvailable\": False,\n \"confidence\": None,\n \"model\": model_name,\n \"coverage\": {\n \"modelRecognized\": model_recognized,\n \"modelMatch\": declared_match,\n \"declaredModel\": declared_model or None,\n \"pricedModel\": model_name,\n \"toolsPriced\": sum(1 for n in _tn if _tool_is_priced(_tc, n)),\n \"toolsTotal\": len(_tn),\n },\n \"lastCalibrated\": defaults.get(\"last_calibrated\"),\n }\n\n # ── Volume: runs/day × turns/run = LLM calls/day ──\n # An agent RUN is an agentic loop of several LLM round-trips (turns). Cost is\n # per LLM call, so monthly cost scales with llm_calls_per_day = runs × turns —\n # not runs alone (the old bug, which under-counted ~4×).\n # runs_per_day : business volume (declared / slider / default)\n # turns_per_run : LLM round-trips per run (measured by sandbox/live, else\n # archetype default)\n # Live tier observes total LLM calls/day directly and must NOT re-multiply by\n # turns — unless the CFO is actively dialing a runs/turns what-if slider.\n # Sandbox-measured averages (medium tier): real turns + tokens from sims.\n # Lower priority than live/explicit overrides, higher than static defaults.\n # Kept SEPARATE from `overrides` so sandbox turns can't masquerade as an\n # explicit what-if and suppress the live-tier branch below.\n sandbox_avgs = compute_sandbox_averages(sandbox_traces) if sandbox_traces else {}\n\n explicit_runs = _first_set(\n overrides.get(\"runs_per_day\"),\n overrides.get(\"calls_per_day\"), # legacy alias = runs at static/sandbox tier\n default=None,\n )\n explicit_turns = overrides.get(\"turns_per_run\")\n observed_llm_calls = overrides.get(\"llm_calls_per_day\")\n\n known_turns = _first_set(\n explicit_turns,\n sandbox_avgs.get(\"turns_per_run\"),\n agent_config.get(\"expected_turns_per_run\"),\n default=None,\n )\n volume_declared = explicit_runs is not None or agent_config.get(\"expected_calls_per_day\") is not None\n if known_turns is not None:\n turns_per_run = max(1, int(round(float(known_turns))))\n elif volume_declared:\n # Declared volume WITHOUT declared/measured turns: the customer's number\n # is their total model-calls/day (what a provider console shows). An\n # archetype guess multiplied it 4-8x invisibly and was the single\n # largest source of forecast error (mean |err| 546% -> 60% on the truth\n # fleet with this rule alone). An assumption may widen a band; it must\n # never multiply a declared number.\n turns_per_run = 1\n else:\n turns_per_run = max(1, int(round(_default_turns_per_run(agent_config, defaults))))\n runs_per_day = int(_first_set(\n explicit_runs,\n agent_config.get(\"expected_calls_per_day\"),\n default=sd[\"default_calls_per_day\"],\n ))\n\n # Did the customer ever opt into runs-x-turns semantics? Only by declaring\n # turns (API/UI) or driving the what-if sliders. A bare declared calls/day\n # is TOTAL model calls (the P1 contract) — and that contract must survive\n # the tier upgrade: when a sweep later MEASURES turns, the measurement\n # refines the run split, it must not re-multiply the declared total (which\n # turned a correct low-tier forecast into a wrong medium-tier one while\n # claiming more confidence).\n volume_is_total = (\n agent_config.get(\"expected_calls_per_day\") is not None\n and explicit_runs is None\n and explicit_turns is None\n and agent_config.get(\"expected_turns_per_run\") is None\n )\n if observed_llm_calls is not None and explicit_runs is None and explicit_turns is None:\n # Live tier, no manual what-if → trust observed total LLM calls.\n llm_calls_per_day = int(observed_llm_calls)\n turns_per_run = round(llm_calls_per_day / runs_per_day, 1) if runs_per_day else turns_per_run\n elif volume_is_total:\n llm_calls_per_day = int(agent_config[\"expected_calls_per_day\"])\n runs_per_day = max(1, int(round(llm_calls_per_day / turns_per_run)))\n else:\n llm_calls_per_day = int(round(runs_per_day * turns_per_run))\n\n runtime = float(overrides.get(\"runtime\") or sd[\"default_runtime_seconds\"])\n # No sandbox term here on purpose — sims can't measure production cache\n # locality (see compute_sandbox_averages). Live rolling averages arrive\n # via `overrides` on the live path.\n cache_hit_rate = float(_first_set(\n overrides.get(\"cache_hit\"),\n default=sd[\"default_cache_hit_rate\"] * 100,\n )) / 100.0\n retry_rate = float(overrides.get(\"retry_rate\", sd[\"default_retry_rate\"] * 100)) / 100.0\n\n # ── Token estimate ──\n # Observed per-turn tokens from live traces (high) or sandbox sims (medium)\n # override the static capability-tree estimate. Real counts already reflect\n # the tokenizer, so model inflation is not re-applied to them.\n # D25 precedence: live-measured > declared context > sandbox-measured >\n # static default. Sandbox mocks return tiny payloads, so their \"measured\"\n # input tokens must never outrank a declared context — a RAG agent that\n # declared an 80k-token context was repriced off 1.4k-token mock traces at\n # HIGHER confidence (-96% under truth). Sims do measure completion size\n # faithfully, so a sandbox output average still refines the estimate.\n try:\n _declared_ctx = int(agent_config.get(\"avg_context_tokens\") or 0)\n except (TypeError, ValueError):\n _declared_ctx = 0\n live_in, live_out = overrides.get(\"input_tokens\"), overrides.get(\"output_tokens\")\n sbx_in, sbx_out = sandbox_avgs.get(\"input_tokens\"), sandbox_avgs.get(\"output_tokens\")\n if live_in is not None and live_out is not None:\n in_tokens, out_tokens = int(live_in), int(live_out)\n elif _declared_ctx > 0:\n in_tokens, out_tokens = _estimate_tokens_per_call(agent_config, defaults, inflation, turns_per_run)\n if sbx_out is not None:\n out_tokens = int(sbx_out)\n elif sbx_in is not None and sbx_out is not None:\n in_tokens, out_tokens = int(sbx_in), int(sbx_out)\n else:\n in_tokens, out_tokens = _estimate_tokens_per_call(agent_config, defaults, inflation, turns_per_run)\n\n # ── Per-call LLM cost ──\n in_price = model_pricing[\"input_per_mtok\"] / 1_000_000.0\n out_price = model_pricing[\"output_per_mtok\"] / 1_000_000.0\n cache_discount = float(model_pricing.get(\"cache_discount\", 0.5))\n cache_factor = 1.0 - (cache_hit_rate * cache_discount)\n retry_factor = 1.0 + retry_rate\n obs_cost = overrides.get(\"llm_cost_per_call\")\n if obs_cost is not None:\n # Live tier: mean of REAL per-call $ — blends the actual model mix\n # (fixes single-model reconstruction for model-switching agents).\n llm_cost_per_call = float(obs_cost)\n else:\n llm_cost_per_call = (in_tokens * in_price + out_tokens * out_price) * cache_factor * retry_factor\n\n # ── Per-call tool cost (weighted by observed action mix when traces exist) ──\n tool_cost_per_call, actions_with_cost, tool_mix_measured = _estimate_tool_cost_per_call(\n agent_config, defaults, sandbox_traces\n )\n\n # ── Infra overhead ──\n # Split into a fixed per-call overhead plus a runtime-proportional compute\n # cost, so a longer-running call actually costs more — this is what makes\n # the \"Runtime / call\" sensitivity lever real instead of structurally zero.\n infra = defaults[\"infrastructure\"]\n infra_base = float(infra.get(\"per_call_overhead_usd\", 0.0))\n compute_per_sec = float(infra.get(\"compute_cost_per_second_usd\", 0.0))\n infra_per_call = infra_base + runtime * compute_per_sec\n\n # ── Monthly totals ──\n # LLM + infra are per-LLM-CALL → scale by llm_calls_per_day (= runs×turns).\n # Tool calls are NOT one-per-turn: a run can't invoke more DISTINCT actions\n # than it exposes, and a specific action fires ~once per run. Without a trace\n # to measure the real mix, scale tool volume by runs × min(turns, n_actions)\n # so a rare write (e.g. a payout among 3 actions in an 8-turn loop) reads as\n # ~1/run, not 2.67/run (= runs×turns/n). A measured mix already reflects the\n # true per-step share, so it keeps the full llm_calls_per_day scale.\n days_per_month = 30\n _n_actions = len(actions_with_cost) or 1\n tool_calls_per_day = (\n llm_calls_per_day if tool_mix_measured\n else runs_per_day * min(turns_per_run, _n_actions)\n )\n monthly_llm = llm_cost_per_call * llm_calls_per_day * days_per_month\n monthly_tools = tool_cost_per_call * tool_calls_per_day * days_per_month\n monthly_infra = infra_per_call * llm_calls_per_day * days_per_month\n monthly_point = monthly_llm + monthly_tools + monthly_infra\n\n monthly_low = monthly_point * band[\"low_multiplier\"]\n monthly_high = monthly_point * band[\"high_multiplier\"]\n\n # ── Composition percentages ──\n if monthly_point > 0:\n tokens_pct = round((monthly_llm / monthly_point) * 100)\n tools_pct = round((monthly_tools / monthly_point) * 100)\n infra_pct = max(0, 100 - tokens_pct - tools_pct)\n else:\n tokens_pct = tools_pct = infra_pct = None # undefined — don't fabricate 70/20/10\n\n # ── Top tool calls ── projected by per-action weight (observed share when\n # traces exist, else uniform 1/N — disclosed via inputSources.toolMix).\n # Uses the tool-call volume (bounded per run), not the raw LLM-call count.\n monthly_calls = tool_calls_per_day * days_per_month\n top_tools: list[dict] = []\n for tool_name, action_name, cost_per, weight in actions_with_cost:\n calls = monthly_calls * weight\n top_tools.append({\n \"tool\": f\"{tool_name}.{action_name}\",\n \"callsPerMonth\": int(round(calls)),\n \"costPer\": round(cost_per, 4),\n \"monthly\": round(calls * cost_per, 2),\n })\n top_tools.sort(key=lambda x: x[\"monthly\"], reverse=True)\n top_tools = top_tools[:4]\n\n # Only a real ~30-day-old snapshot makes the comparison meaningful. Without\n # one, vs_last_month is 0 — but that's \"no baseline\", not \"flat\". Surface the\n # distinction so the UI can hide the stat instead of implying no change.\n vs_last_month_available = bool(previous_snapshot_point and previous_snapshot_point > 0)\n if vs_last_month_available:\n vs_last_month = int(round((monthly_point - previous_snapshot_point) / previous_snapshot_point * 100))\n else:\n vs_last_month = 0\n\n # ── Coverage summary (disclosure only — not used in the math above) ──\n _tool_names = [(t.get(\"name\") or \"\").lower() for t in agent_config.get(\"tools\", []) if t.get(\"name\")]\n _tac = defaults.get(\"tool_action_costs\", {}) or {}\n tools_total = len(_tool_names)\n tools_priced = sum(1 for n in _tool_names if _tool_is_priced(_tac, n))\n\n # ── Per-input source disclosure: declared / measured / default ──\n # So a defaulted input is never silently presented as a measurement.\n _live_path = observed_llm_calls is not None and explicit_runs is None and explicit_turns is None\n if explicit_runs is not None or agent_config.get(\"expected_calls_per_day\") is not None:\n runs_source = \"declared\"\n else:\n runs_source = \"default\"\n if _live_path:\n turns_source = \"measured\"\n elif explicit_turns is not None:\n turns_source = \"declared\"\n elif sandbox_avgs.get(\"turns_per_run\") is not None:\n turns_source = \"measured\"\n elif agent_config.get(\"expected_turns_per_run\") is not None:\n turns_source = \"declared\"\n elif volume_declared:\n # turns=1 by the P1 rule: declared volume counts total model calls.\n turns_source = \"volume\"\n else:\n turns_source = \"default\"\n # Mirrors the D25 precedence above: only live data may claim \"measured\"\n # over a declared context; sandbox tokens are \"measured\" only when nothing\n # was declared.\n if overrides.get(\"llm_cost_per_call\") is not None or overrides.get(\"input_tokens\") is not None:\n tokens_source = \"measured\"\n elif _declared_ctx > 0:\n tokens_source = \"declared\"\n elif sandbox_avgs.get(\"input_tokens\") is not None:\n tokens_source = \"measured\"\n else:\n tokens_source = \"default\"\n if _live_path:\n cache_source = \"measured\"\n elif overrides.get(\"cache_hit\") is not None:\n cache_source = \"declared\"\n else:\n cache_source = \"default\"\n model_source = \"declared\" if (overrides.get(\"model\") or (declared_model and declared_match != \"default\")) else \"default\"\n input_sources = {\n \"runsPerDay\": runs_source,\n \"turnsPerRun\": turns_source,\n \"tokensPerCall\": tokens_source,\n \"cacheHit\": cache_source,\n \"model\": model_source,\n \"toolMix\": \"measured\" if tool_mix_measured else \"default\",\n }\n\n return {\n \"available\": True,\n \"point\": round(monthly_point),\n # Unrounded point — used by the sensitivity engine so small but real\n # levers (runtime, retry) aren't lost to whole-dollar rounding.\n \"pointExact\": monthly_point,\n # Days of live traffic behind the averages (live tier only) — the UI\n # captions the forecast with this so a burst never masquerades as a month.\n \"observedDays\": (overrides.get(\"observed_days\") if _live_path else None),\n # Distinct days with traffic, and whether the burst guard demoted an\n # over-the-call-floor agent from high to medium because of it.\n \"activeDays\": (overrides.get(\"active_days\") if _live_path else None),\n \"confidenceCap\": (\"single_day_burst\" if burst_capped else None),\n \"low\": round(monthly_low),\n \"high\": round(monthly_high),\n # Derive annual from the ROUNDED monthly shown on the card, so a CFO who\n # multiplies the displayed monthly by 12 gets exactly this number.\n \"annual\": round(monthly_point) * 12,\n \"vsLastMonth\": vs_last_month,\n \"vsLastMonthAvailable\": vs_last_month_available,\n \"callsPerDay\": llm_calls_per_day, # total LLM calls/day (= runs × turns); back-compat key\n \"runsPerDay\": runs_per_day,\n \"turnsPerRun\": turns_per_run,\n \"runtime\": runtime,\n \"tokensPerCall\": in_tokens + out_tokens,\n \"cacheHit\": int(cache_hit_rate * 100),\n \"retryRate\": int(retry_rate * 100),\n \"tokensPct\": tokens_pct,\n \"toolsPct\": tools_pct,\n \"infraPct\": infra_pct,\n \"tokensUsd\": round(monthly_llm),\n \"toolsUsd\": round(monthly_tools),\n \"infraUsd\": round(monthly_infra),\n \"topTools\": top_tools,\n # Unit econ outcomes are per-RUN (sandbox occurrences are per run) → pass runs_per_day, not llm_calls.\n \"unitEcon\": _compute_unit_econ(agent_config, sandbox_traces, monthly_point, runs_per_day),\n \"sensitivity\": (\n [] # never canned; sensitivity is real per-agent or absent\n if _skip_sensitivity\n else _cached_sensitivity(\n agent_config, sandbox_traces, live_trace_count_7d, overrides, monthly_point, defaults,\n org_id=org_id,\n )\n ),\n \"inputSources\": input_sources,\n \"confidence\": tier,\n \"model\": model_name,\n \"coverage\": {\n \"modelRecognized\": model_recognized,\n \"modelMatch\": declared_match,\n \"declaredModel\": declared_model or None,\n \"pricedModel\": model_name,\n \"toolsPriced\": tools_priced,\n \"toolsTotal\": tools_total,\n },\n \"lastCalibrated\": defaults.get(\"last_calibrated\"),\n }\n\n\n# ── Budget-fit recommender (CFO: \"I have $X — does it fit, and how?\") ─────────\n\ndef compute_budget_fit(\n agent_config: dict,\n *,\n budget: float,\n base_overrides: Optional[dict] = None,\n org_id: Optional[str] = None,\n cost_report_items: Optional[list] = None,\n) -> dict:\n \"\"\"Given a monthly budget, say whether the agent fits and — if over — return\n a ranked list of honest levers to close the gap.\n\n This is an *options engine*, not an optimizer: the cost model knows cost,\n not quality, so every quality-affecting lever carries a plain-English\n tradeoff and the human decides. `cost_report_items` (from /cost-report)\n powers the risk-reduction crossover on the action-gating lever.\n\n Returns {budget, forecastPoint, gap, status (\"under\"|\"over\"),\n currentModel, recommendations: [{lever, label, projectedSaving,\n newPoint, tradeoff, riskReductionUsd?}]}.\n \"\"\"\n base_overrides = dict(base_overrides or {})\n\n def _point(extra: dict) -> tuple[int, dict]:\n f = forecast_spend(\n agent_config, overrides={**base_overrides, **extra} or None,\n org_id=org_id, _skip_sensitivity=True,\n )\n return f[\"point\"], f\n\n base_point, base = _point({})\n if not base.get(\"available\") or base_point is None:\n # Can't fit a budget to a forecast we don't have — need data first.\n return {\n \"available\": False,\n \"reason\": \"no_data\",\n \"needs\": [\"declare_volume\", \"sandbox_sweep\", \"live_traces\"],\n \"budget\": round(budget),\n \"forecastPoint\": None,\n \"recommendations\": [],\n }\n gap = base_point - budget\n status = \"over\" if gap > 0 else \"under\"\n result = {\n \"budget\": round(budget),\n \"forecastPoint\": base_point,\n \"gap\": round(gap),\n \"status\": status,\n \"currentModel\": base[\"model\"],\n \"recommendations\": [],\n }\n if gap <= 0:\n return result\n\n cost_recs: list[dict] = []\n\n # ── Lever 1: model tier — least-aggressive downgrade that fits, else cheapest\n cur_model = base_overrides.get(\"model\") or base[\"model\"]\n cheaper: list[tuple[str, int]] = []\n for m_name in (load_defaults(org_id).get(\"models\", {})):\n if m_name == cur_model:\n continue\n p, _ = _point({\"model\": m_name})\n if p < base_point:\n cheaper.append((m_name, p))\n if cheaper:\n fitting = [c for c in cheaper if c[1] <= budget]\n # Among models that fit, prefer the most expensive (smallest change);\n # if none fit, take the cheapest (max saving).\n pick = max(fitting, key=lambda c: c[1]) if fitting else min(cheaper, key=lambda c: c[1])\n cost_recs.append({\n \"lever\": \"model\",\n \"label\": f\"Switch the model to {pick[0]}\",\n \"projectedSaving\": round(base_point - pick[1]),\n \"newPoint\": pick[1],\n \"tradeoff\": \"Cheaper per token — verify quality on your evals before switching.\",\n })\n\n # ── Lever 2: cache hit rate — only if there's real headroom\n cur_cache = base[\"cacheHit\"]\n if cur_cache < 80:\n p, _ = _point({\"cache_hit\": 80})\n if p < base_point:\n cost_recs.append({\n \"lever\": \"cache\",\n \"label\": f\"Raise cache hit rate to 80% (now {cur_cache}%)\",\n \"projectedSaving\": round(base_point - p),\n \"newPoint\": p,\n \"tradeoff\": \"Depends on reusing a stable system prompt — not a dial you just turn.\",\n })\n\n # ── Lever 3: call volume — the always-available \"do less work\" lever.\n # Operate on RUNS/day (the business lever); cost scales linearly so the same\n # ratio applies whether expressed as runs or LLM calls.\n cur_runs = base.get(\"runsPerDay\") or base[\"callsPerDay\"]\n if base_point > 0:\n target_runs = max(1, int(cur_runs * budget / base_point))\n if target_runs < cur_runs:\n p, _ = _point({\"runs_per_day\": target_runs})\n cost_recs.append({\n \"lever\": \"volume\",\n \"label\": f\"Cut usage to ~{target_runs} runs/day (now {cur_runs})\",\n \"projectedSaving\": round(base_point - p),\n \"newPoint\": p,\n \"tradeoff\": \"This is doing less work — fewer runs, not cheaper runs.\",\n })\n\n cost_recs.sort(key=lambda r: r[\"projectedSaving\"], reverse=True)\n\n # ── Lever 4 (kept separate so it isn't crowded out): gate the riskiest\n # unprotected action. Mostly a RISK lever — surfaces the cost+risk crossover.\n gate_rec = None\n if cost_report_items:\n unprotected = [i for i in cost_report_items if not i.get(\"has_policy\")]\n if unprotected:\n worst = max(unprotected, key=lambda i: i.get(\"per_incident_max_usd\", 0) or 0)\n action_key = f\"{worst.get('tool')}.{worst.get('action')}\"\n tool_saving = 0\n for t in base.get(\"topTools\", []):\n if t.get(\"tool\") == action_key:\n tool_saving = t.get(\"monthly\", 0)\n break\n risk_usd = round(worst.get(\"per_incident_max_usd\", 0) or 0)\n if risk_usd > 0:\n gate_rec = {\n \"lever\": \"gate\",\n \"label\": f\"Require approval on {action_key}\",\n \"projectedSaving\": round(tool_saving),\n \"newPoint\": round(base_point - tool_saving),\n \"tradeoff\": \"Adds a human check on that action — small cost impact, large risk cut.\",\n \"riskReductionUsd\": risk_usd,\n }\n\n recs = cost_recs[:3]\n if gate_rec:\n recs.append(gate_rec)\n result[\"recommendations\"] = recs\n return result\n"}}}}