1 line
83 KiB
JSON
1 line
83 KiB
JSON
{"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"}}}} |