floe-guard 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/livekit.cjs +24 -0
- package/dist/adapters/livekit.cjs.map +1 -1
- package/dist/adapters/livekit.d.cts +1 -1
- package/dist/adapters/livekit.d.ts +1 -1
- package/dist/adapters/livekit.js +1 -1
- package/dist/adapters/retell.cjs +24 -0
- package/dist/adapters/retell.cjs.map +1 -1
- package/dist/adapters/retell.d.cts +1 -1
- package/dist/adapters/retell.d.ts +1 -1
- package/dist/adapters/retell.js +1 -1
- package/dist/adapters/vapi.cjs +24 -0
- package/dist/adapters/vapi.cjs.map +1 -1
- package/dist/adapters/vapi.d.cts +2 -2
- package/dist/adapters/vapi.d.ts +2 -2
- package/dist/adapters/vapi.js +1 -1
- package/dist/{chunk-5Q5VQC6J.js → chunk-TAK6I7CN.js} +25 -1
- package/dist/chunk-TAK6I7CN.js.map +1 -0
- package/dist/{guard-BXtXoark.d.cts → guard-Ct38zQZS.d.cts} +6 -4
- package/dist/{guard-BXtXoark.d.ts → guard-Ct38zQZS.d.ts} +6 -4
- package/dist/index.cjs +42 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -4
- package/dist/index.d.ts +13 -4
- package/dist/index.js +19 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cost_map.json +24 -0
- package/dist/chunk-5Q5VQC6J.js.map +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sync.ts","../src/pricing.ts","../src/guard.ts","../src/latency.ts","../src/middleware.ts","../src/retry.ts"],"sourcesContent":["/**\n * Opt-in ledger sync — push the local spend ledger to Floe's Reconcile Mode.\n *\n * **Zero-telemetry is the default, and stays the default.** Nothing in this module\n * runs unless the caller *explicitly* opts in with a Floe API key — via\n * {@link BudgetGuard.enableSync} + {@link BudgetGuard.sync}. There is no implicit\n * enablement and no background send: the ledger leaves your process only when you\n * call one of those. Your ledger, your key, your choice.\n *\n * **Why sync at all.** Floe's gateway can't see spend it never routed — BYOK,\n * self-hosted, or off-path LLM/tool calls. Pushing your local ledger into Reconcile\n * Mode is how that spend lands on the ledger and your **Coverage Score** becomes\n * computable. Budget, not balance: this reports *what you already spent* for\n * coverage/attribution; it does not move money or change any wallet balance.\n *\n * **What leaves the process — exactly the {@link BudgetGuard.exportLog} JSONL** and\n * nothing else: one line per spend event, each a priced-cost record — `timestamp`,\n * `kind` (`\"llm\"`/`\"tool\"`), `model_or_tool`, `prompt_tokens`, `completion_tokens`,\n * `cost_usd`, and the optional `label` / `reserved` you set. **No prompts, no\n * message content, no identifiers** beyond a `label` you choose.\n *\n * Mirrors `src/floe_guard/sync.py` in the Python package — same behaviour\n * (https-only, key-required, fail-closed), transported over `fetch` instead of\n * `urllib`.\n *\n * Opt in: https://dev-dashboard.floelabs.xyz · https://floelabs.xyz\n */\n\nimport { LedgerSyncError } from \"./errors.js\";\n\nconst FLOE_API_KEY_ENV = \"FLOE_API_KEY\";\nconst FLOE_API_BASE_URL_ENV = \"FLOE_API_BASE_URL\";\nconst DEFAULT_BASE_URL = \"https://credit-api.floelabs.xyz\";\nconst LEDGER_SYNC_PATH = \"/v1/agents/ledger/sync\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n// The ONLY keys allowed to leave the process — the exportLog() schema. Enforced\n// (not just documented) in validateLedger(): a line carrying anything else is\n// rejected, so a hand-supplied file can't smuggle prompts / content / identifiers\n// past the privacy contract.\nconst ALLOWED_KEYS = new Set([\n \"timestamp\",\n \"kind\",\n \"model_or_tool\",\n \"prompt_tokens\",\n \"completion_tokens\",\n \"cost_usd\",\n \"label\",\n \"reserved\",\n]);\nconst REQUIRED_KEYS = [\"timestamp\", \"kind\", \"model_or_tool\", \"cost_usd\"];\n\n/**\n * Read an environment variable without a hard `process` reference — the package\n * compiles with `types: []` (no `@types/node`), so `process` is untyped. Reads\n * live at call time so tests that mutate the env are honoured.\n */\nfunction envVar(name: string): string {\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n return proc?.env?.[name] ?? \"\";\n}\n\n/**\n * Parse and validate every ledger line against the `exportLog()` schema.\n *\n * Throws {@link LedgerSyncError} on the first malformed / unknown-field / invalid\n * record, **before** anything is sent — so only priced spend events ever leave the\n * process, even when the ledger came from a hand-edited file. This is the privacy\n * contract *enforced*, not merely asserted: an **extra/unknown key** (a smuggled\n * `prompt`, `user_id`, …) is rejected outright. Mirrors `_validate_ledger` in the\n * Python client.\n *\n * (JS `typeof x === \"number\"` already excludes booleans and `Number.isInteger`\n * rejects them too, so — unlike Python, where `bool` is an `int` subclass — no\n * explicit boolean guards are needed.)\n */\nfunction validateLedger(jsonl: string): void {\n let i = 0;\n for (const raw of jsonl.split(\"\\n\")) {\n i += 1;\n const line = raw.trim();\n if (!line) continue;\n let event: unknown;\n try {\n event = JSON.parse(line);\n } catch (err) {\n throw new LedgerSyncError(`Ledger line ${i} is not valid JSON: ${describeCause(err)}`);\n }\n if (typeof event !== \"object\" || event === null || Array.isArray(event)) {\n throw new LedgerSyncError(`Ledger line ${i} is not a JSON object.`);\n }\n const ev = event as Record<string, unknown>;\n const extra = Object.keys(ev).filter((k) => !ALLOWED_KEYS.has(k));\n if (extra.length) {\n throw new LedgerSyncError(\n `Ledger line ${i} has fields outside the exportLog() schema: ` +\n `${JSON.stringify(extra.sort())}. Only priced spend events may be synced — ` +\n \"no prompts, content, or identifiers.\",\n );\n }\n const missing = REQUIRED_KEYS.filter((k) => !(k in ev));\n if (missing.length) {\n throw new LedgerSyncError(\n `Ledger line ${i} is missing required field(s): ${JSON.stringify(missing)}.`,\n );\n }\n if (ev.kind !== \"llm\" && ev.kind !== \"tool\") {\n throw new LedgerSyncError(\n `Ledger line ${i}: kind must be 'llm' or 'tool', got ${JSON.stringify(ev.kind)}.`,\n );\n }\n if (typeof ev.model_or_tool !== \"string\") {\n throw new LedgerSyncError(`Ledger line ${i}: model_or_tool must be a string.`);\n }\n const cost = ev.cost_usd;\n if (typeof cost !== \"number\" || !Number.isFinite(cost) || cost < 0) {\n throw new LedgerSyncError(\n `Ledger line ${i}: cost_usd must be a finite, non-negative number, got ${JSON.stringify(cost)}.`,\n );\n }\n const ts = ev.timestamp;\n if (typeof ts !== \"number\" || !Number.isFinite(ts)) {\n throw new LedgerSyncError(\n `Ledger line ${i}: timestamp must be a finite number, got ${JSON.stringify(ts)}.`,\n );\n }\n for (const field of [\"prompt_tokens\", \"completion_tokens\"] as const) {\n const value = ev[field];\n // Absent (undefined) or explicit null are fine — tool events carry null; a\n // present value must be an integer.\n if (value !== undefined && value !== null && !Number.isInteger(value)) {\n throw new LedgerSyncError(`Ledger line ${i}: ${field} must be an integer or null.`);\n }\n }\n if (\"label\" in ev && typeof ev.label !== \"string\") {\n throw new LedgerSyncError(`Ledger line ${i}: label must be a string.`);\n }\n if (\"reserved\" in ev) {\n const reserved = ev.reserved;\n if (typeof reserved !== \"number\" || !Number.isFinite(reserved)) {\n throw new LedgerSyncError(`Ledger line ${i}: reserved must be a finite number.`);\n }\n }\n }\n}\n\n/**\n * POST an {@link BudgetGuard.exportLog} JSONL ledger to Reconcile Mode and resolve\n * with the number of events the server accepted.\n *\n * This is the ONLY function that sends the ledger, and it sends only when called.\n * An empty ledger is a no-op (resolves `0` with **no** `fetch` call).\n *\n * @param jsonl the ledger as newline-delimited JSON — `exportLog()` output.\n * @param apiKey Floe agent key (`floe_<hex>`, `read_write`). Defaults to the\n * `FLOE_API_KEY` env var.\n * @param opts.baseUrl API base. Defaults to `FLOE_API_BASE_URL`, else the prod host.\n * @param opts.timeoutMs request timeout in milliseconds (default 30_000).\n * @throws {LedgerSyncError} missing key, a non-https/malformed base URL, a non-2xx\n * response, network/timeout, or a malformed response body. The key and ledger are\n * never sent over a non-https or malformed URL.\n */\nexport async function pushLedger(\n jsonl: string,\n apiKey?: string,\n opts: { baseUrl?: string; timeoutMs?: number } = {},\n): Promise<number> {\n // An empty ledger never touches the network — nothing to send.\n if (!jsonl.trim()) return 0;\n\n // Enforce the privacy contract BEFORE any network: only exportLog() events —\n // no prompts, content, or identifiers — may leave, even from a hand-supplied file.\n validateLedger(jsonl);\n\n const key = ((apiKey ?? \"\") || envVar(FLOE_API_KEY_ENV)).trim();\n if (!key) {\n throw new LedgerSyncError(\n `No Floe API key: pass apiKey or set ${FLOE_API_KEY_ENV}. ` +\n \"Sync is opt-in and needs your key.\",\n );\n }\n\n const envBase = envVar(FLOE_API_BASE_URL_ENV).trim();\n const base = ((opts.baseUrl ?? \"\").trim() || envBase || DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n let parsed: URL;\n try {\n parsed = new URL(base);\n } catch {\n // The request carries the Floe agent key as a bearer token AND your spend\n // ledger — never send either over a malformed URL.\n throw new LedgerSyncError(\n `Refusing to send the ledger to '${base}': ` +\n \"the base URL must be an https:// URL with a host.\",\n );\n }\n if (parsed.protocol !== \"https:\" || !parsed.host) {\n // Same guard for a well-formed but non-https (or hostless) URL.\n throw new LedgerSyncError(\n `Refusing to send the ledger to '${base}': ` +\n \"the base URL must be an https:// URL with a host.\",\n );\n }\n const url = `${base}${LEDGER_SYNC_PATH}`;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n // Refuse redirects: a 3xx would otherwise let the key + ledger be re-sent to\n // another host. (The Fetch spec strips `Authorization` on a cross-origin\n // redirect, but `redirect: \"error\"` is the belt-and-suspenders — nothing is\n // re-sent anywhere, same-origin or not.)\n redirect: \"error\",\n headers: {\n Authorization: `Bearer ${key}`,\n \"Content-Type\": \"application/x-ndjson\",\n Accept: \"application/json\",\n },\n body: jsonl,\n signal: controller.signal,\n });\n } catch (err) {\n throw new LedgerSyncError(`Could not reach Floe at ${url}: ${describeCause(err)}`);\n } finally {\n clearTimeout(timer);\n }\n\n if (!response.ok) {\n throw new LedgerSyncError(await describeHttpError(response));\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(await response.text());\n } catch (err) {\n throw new LedgerSyncError(`Malformed JSON from Floe at ${url}: ${describeCause(err)}`);\n }\n if (typeof payload !== \"object\" || payload === null || Array.isArray(payload)) {\n throw new LedgerSyncError(`Unexpected response shape from Floe at ${url}.`);\n }\n // \"synced\" is the count the server accepted (new rows); \"duplicates\" (already\n // ingested, idempotent) are not counted here. Absent → 0; present must be a real\n // non-negative integer — reject bool/float/negative/garbage rather than coerce.\n const synced = (payload as Record<string, unknown>).synced;\n if (synced === undefined || synced === null) return 0;\n if (typeof synced !== \"number\" || !Number.isInteger(synced) || synced < 0) {\n throw new LedgerSyncError(`Invalid 'synced' count from Floe at ${url}: ${JSON.stringify(synced)}.`);\n }\n return synced;\n}\n\nfunction describeCause(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nasync function describeHttpError(response: Response): Promise<string> {\n let detail = \"\";\n try {\n const data: unknown = JSON.parse(await response.text());\n if (\n typeof data === \"object\" &&\n data !== null &&\n typeof (data as Record<string, unknown>).error === \"string\"\n ) {\n detail = ` (${(data as Record<string, unknown>).error as string})`;\n }\n } catch {\n // Non-JSON / empty error body — the status code alone is the message.\n }\n const code = response.status;\n if (code === 401) return `Floe rejected the API key (401 unauthorized)${detail}.`;\n if (code === 403) {\n return (\n `Floe refused the sync (403 forbidden)${detail} — the agent may be ` +\n \"closed/suspended, or the key is read-only (a read_write key is required).\"\n );\n }\n return `Floe returned HTTP ${code}${detail}.`;\n}\n","/**\n * Offline token pricing from a vendored LiteLLM cost map.\n *\n * Mirrors `src/floe_guard/pricing.py`: BOTH the input and output per-token prices\n * must be finite numbers, otherwise the model is treated as unpriceable. A\n * half-valid entry would silently undercharge — so we refuse it (fail-closed).\n *\n * No network. The cost map (`cost_map.json`) is a snapshot of LiteLLM's\n * `model_prices_and_context_window.json`; refresh it on a schedule or estimates\n * drift as vendors change prices.\n */\n\nimport costMapJson from \"./cost_map.json\";\n\n/** A user-supplied per-token price, in USD, for a model the map cannot price. */\nexport interface ManualPrice {\n inputCostPerToken: number;\n outputCostPerToken: number;\n}\n\n/** A resolved per-token price plus where it came from. */\nexport interface PricedModel {\n inputCostPerToken: number;\n outputCostPerToken: number;\n source: \"override\" | \"cost_map\";\n}\n\ninterface CostMapEntry {\n input_cost_per_token?: unknown;\n output_cost_per_token?: unknown;\n}\n\nconst COST_MAP = costMapJson as Record<string, CostMapEntry>;\n\n/**\n * The one `<provider>/` prefix that is safe to strip: the remainder of a\n * `groq/…` id is the ChatGroq id the map vendors (e.g. `groq/qwen/qwen3-32b` →\n * `qwen/qwen3-32b`). `openai/` and `anthropic/` are deliberately excluded:\n * their own model ids never contain slashes (single-segment remainders are\n * already covered by the bare-last-segment fallback), so a multi-segment\n * remainder under those prefixes is some OTHER vendor's model behind an\n * OpenAI-compatible endpoint (vLLM, OpenRouter, …) — bridging it into a\n * Groq-priced key would under-meter. Unknown prefixes fail closed the same way.\n */\nconst PROVIDER_PREFIXES = new Set([\"groq\"]);\n\n/**\n * A trailing dated-snapshot suffix: Anthropic's `-20250929` or OpenAI's\n * `-2024-08-06`. Vendors resolve alias ids to dated snapshots in responses, so a\n * snapshot the map doesn't list yet prices at its alias entry (same model, same\n * rate) instead of failing closed. (`\\d` is ASCII-only in JS; the Python regex\n * uses re.ASCII to match.)\n */\nconst DATE_SUFFIX = /-(?:\\d{8}|\\d{4}-\\d{2}-\\d{2})$/;\n\n/**\n * Lookup keys for a model id in two specificity groups, deduplicated.\n *\n * Group 1 (exact): the raw id, the id with a known `provider/` first segment\n * stripped, the bare last segment. Group 2 (date-stripped): the same forms\n * with a trailing dated-snapshot suffix removed. Kept separate so a\n * less-specific date-stripped key (in overrides OR the map) can never shadow\n * an exact dated entry — e.g. an alias override must not absorb a snapshot\n * the map prices differently.\n */\nfunction candidateGroups(model: string): [string[], string[]] {\n const m = model.trim();\n const base = [m];\n const firstSlash = m.indexOf(\"/\");\n if (firstSlash !== -1 && PROVIDER_PREFIXES.has(m.slice(0, firstSlash))) {\n base.push(m.slice(firstSlash + 1));\n }\n const lastSlash = m.lastIndexOf(\"/\");\n if (lastSlash !== -1) {\n base.push(m.slice(lastSlash + 1));\n }\n const exact: string[] = [];\n for (const cand of base) {\n if (cand && !exact.includes(cand)) exact.push(cand);\n }\n const stripped: string[] = [];\n for (const cand of exact) {\n const c = cand.replace(DATE_SUFFIX, \"\");\n if (c && !exact.includes(c) && !stripped.includes(c)) stripped.push(c);\n }\n return [exact, stripped];\n}\n\nfunction bothFinite(a: unknown, b: unknown): boolean {\n return (\n typeof a === \"number\" &&\n typeof b === \"number\" &&\n Number.isFinite(a) &&\n Number.isFinite(b)\n );\n}\n\n/**\n * Resolve a model to its per-token price, or `null` if it cannot be priced.\n *\n * Per specificity group (exact forms first, date-stripped fallbacks second):\n * overrides win, then the bundled cost map. Fail-closed: the first matching\n * entry must have finite prices, else `null`.\n */\nexport function resolvePrice(\n model: string,\n overrides?: Record<string, ManualPrice>,\n): PricedModel | null {\n for (const cands of candidateGroups(model)) {\n if (overrides) {\n for (const cand of cands) {\n // Own-property check so a key like \"constructor\" can't pull a function\n // off Object.prototype and be treated as a price entry.\n const ov = Object.prototype.hasOwnProperty.call(overrides, cand)\n ? overrides[cand]\n : undefined;\n if (ov !== undefined) {\n if (bothFinite(ov.inputCostPerToken, ov.outputCostPerToken)) {\n return {\n inputCostPerToken: ov.inputCostPerToken,\n outputCostPerToken: ov.outputCostPerToken,\n source: \"override\",\n };\n }\n return null;\n }\n }\n }\n\n for (const cand of cands) {\n const entry = Object.prototype.hasOwnProperty.call(COST_MAP, cand)\n ? COST_MAP[cand]\n : undefined;\n if (!entry) continue;\n\n const input = entry.input_cost_per_token;\n const output = entry.output_cost_per_token;\n if (!bothFinite(input, output)) return null;\n\n return {\n inputCostPerToken: input as number,\n outputCostPerToken: output as number,\n source: \"cost_map\",\n };\n }\n }\n return null;\n}\n\n/** USD cost for token usage. Negative counts are clamped to zero. */\nexport function priceTokens(\n priced: PricedModel,\n promptTokens: number,\n completionTokens: number,\n): number {\n const p = Math.max(0, promptTokens);\n const c = Math.max(0, completionTokens);\n const cost = p * priced.inputCostPerToken + c * priced.outputCostPerToken;\n if (!Number.isFinite(cost)) {\n // Defense-in-depth: resolvePrice already guarantees finite rates.\n throw new Error(\"Non-finite LLM cost — pricing entry is invalid\");\n }\n return Math.max(0, cost);\n}\n","/**\n * The local, in-process budget guard.\n *\n * `BudgetGuard` is a kill-switch that lives in the LLM call path. The contract:\n *\n * 1. Call {@link BudgetGuard.check} BEFORE every LLM call. If the *next* call\n * would cross the ceiling, it throws {@link BudgetExceeded} and the call never\n * runs.\n * 2. Call {@link BudgetGuard.record} AFTER every response, with the token usage.\n * It prices the tokens offline and accrues the USD into a running total.\n *\n * **Concurrency.** `check()` then `record()` is a check-then-act with an `await`\n * in between. Fire several model calls at once (e.g. `Promise.all`) and they all\n * `check()` against the same under-limit total before any `record()` lands, so\n * the ceiling is blown (see issue #18). {@link BudgetGuard.reserve} /\n * {@link BudgetGuard.settle} close that gap: `reserve()` holds the estimated cost\n * in flight (synchronously, before the await), so parallel callers each take\n * their own slice of the ceiling. JS is single-threaded, so an in-flight counter\n * is enough — no lock needed. The middleware uses it; `check`/`record` are\n * unchanged.\n *\n * This is a faithful port of `src/floe_guard/guard.py` — same prediction logic,\n * same epsilon handling, same fail-closed default.\n */\n\nimport { BudgetExceeded, TokenBudgetExceeded, UnpriceableModelError } from \"./errors.js\";\nimport { pushLedger } from \"./sync.js\";\nimport {\n type ManualPrice,\n priceTokens,\n resolvePrice,\n} from \"./pricing.js\";\n\n/** Tolerance for float rounding in the running spend total (well below $0.000001). */\nconst EPS = 1e-12;\n\n/**\n * A two-dimensional in-flight hold (USD + tokens) returned by\n * {@link BudgetGuard.reserve} when a token ceiling or an active\n * {@link BudgetGuard.step} is involved. A dumb value handle — pass it straight\n * back to {@link BudgetGuard.settle} / {@link BudgetGuard.release}. When neither\n * tokens nor a step are involved, `reserve` returns a plain `number` instead\n * (byte-for-byte the old behaviour), so {@link ReservationHandle} is the union.\n */\nexport interface BudgetReservation {\n readonly usd: number;\n readonly tokens: number;\n}\n\n/**\n * A plain `number` when USD-only (backward compatible); a\n * {@link BudgetReservation} when a token ceiling or an active step is in play.\n */\nexport type ReservationHandle = number | BudgetReservation;\n\nfunction isReservation(h: ReservationHandle): h is BudgetReservation {\n return (\n typeof h === \"object\" &&\n h !== null &&\n typeof (h as BudgetReservation).usd === \"number\" &&\n typeof (h as BudgetReservation).tokens === \"number\"\n );\n}\n\n/** One scope on the step stack (see {@link BudgetGuard.step}). Mutable. */\ninterface StepState {\n readonly maxUsd: number | null;\n readonly maxTokens: number | null;\n spentUsd: number;\n spentTokens: number;\n reservedUsd: number;\n reservedTokens: number;\n}\n\n/**\n * One priced spend event in the guard's per-call ledger.\n *\n * Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /\n * {@link BudgetGuard.recordTool} / {@link BudgetGuard.settleTool} that accrues\n * spend appends exactly one event, so\n * the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has\n * evicted old events). The schema is identical in the Python\n * package (`SpendEvent` in `src/floe_guard/guard.py`) and\n * {@link BudgetGuard.exportLog} serialises it with the same snake_case keys in\n * both languages, so every agent emits the same shape regardless of stack.\n */\nexport interface SpendEvent {\n /** Unix epoch seconds (UTC). */\n readonly timestamp: number;\n readonly kind: \"llm\" | \"tool\";\n readonly modelOrTool: string;\n /** `null` for tool events. */\n readonly promptTokens: number | null;\n /** `null` for tool events. */\n readonly completionTokens: number | null;\n readonly costUsd: number;\n /** Caller-supplied tag (agent/task name). */\n readonly label?: string;\n /** The reservation settled by this call, if any. */\n readonly reserved?: number;\n}\n\nexport interface BudgetGuardOptions {\n /** Per-model manual prices for models the bundled cost map cannot price. */\n priceOverrides?: Record<string, ManualPrice>;\n /**\n * When `true` (default), recording an unpriceable model without a manual price\n * warns loudly AND throws {@link UnpriceableModelError}. When `false`, it warns\n * and skips accrual (you have opted into un-enforced spend for that model).\n */\n failClosed?: boolean;\n /**\n * Optional callback invoked with `(spentUsd, limitUsd)` right before\n * {@link BudgetExceeded} is thrown. Defaults to printing the\n * `BUDGET EXCEEDED — call blocked` banner to stderr. Fires on **USD** ceiling\n * crossings only; a token-ceiling block ({@link TokenBudgetExceeded}, aggregate\n * or step) is thrown without invoking it, since the callback is dollar-shaped.\n */\n onBlock?: (spentUsd: number, limitUsd: number) => void;\n /**\n * Utilization (basis points, 0..10000) at which {@link BudgetGuard.advisory}\n * flags `nearLimit` so an agent can taper before the hard-stop. Default 8000.\n */\n nearLimitBps?: number;\n /**\n * Optional cap on the per-call spend ledger ({@link BudgetGuard.spendLog}).\n * When set, the ledger is a ring buffer keeping the most recent N events so a\n * long-running agent's memory stays bounded; the running totals are\n * unaffected. Default: keep every event.\n */\n maxLogEvents?: number;\n /**\n * Aggregate token ceiling. `null`/undefined disables the token dimension\n * entirely (a pure USD guard, unchanged). `0` blocks the first token.\n */\n tokenLimit?: number;\n}\n\n/**\n * A context-aware spend signal for the single local budget.\n *\n * Mirrors the core fields of hosted Floe's `X-Floe-Budget-Advisory` header, so\n * agent logic that reads it (taper as you approach the cap, stop at it) ports\n * unchanged to the hosted path. Hosted adds what a local, single-budget guard\n * cannot know: which of several caps is tightest (`scope` across\n * `credit_line | session | task | api | vendor`), cross-vendor reasoning,\n * server-truth balances, and rolling-window reset timing.\n *\n * This is a **soft** signal — the model may ignore it. The hard-stop\n * ({@link BudgetGuard.check}) is what enforces the ceiling; the advisory is\n * upside (let the agent finish on budget rather than be cut off).\n */\nexport interface BudgetAdvisory {\n nearLimit: boolean;\n /** Utilization in basis points, 0..10000 (8500 = 85%). */\n usedBps: number;\n remainingUsd: number;\n limitUsd: number;\n spentUsd: number;\n /** Hosted reports the tightest cap across all scopes; local is always \"local\". */\n scope: \"local\";\n /**\n * The guard's own next-call estimate (the costlier of the last LLM and last\n * tool call — the same value the default reservation uses). 0 until the first\n * call is recorded, so a planner can't divide by a cold estimate.\n *\n * Optional so adding it stays a non-breaking, additive change for any code\n * that constructs a `BudgetAdvisory` literal; `advisory()` always sets it.\n */\n expectedCost?: number;\n /**\n * How many more calls the remaining budget buys at expectedCost:\n * floor(remainingUsd / expectedCost). null when expectedCost is 0 (no call\n * recorded yet) — unknown, not zero. Optional for the same additive reason as\n * expectedCost; `advisory()` always sets it.\n */\n estCallsRemaining?: number | null;\n /**\n * Aggregate token utilization in basis points (0..10000), mirroring usedBps\n * for the token ceiling. null when no `tokenLimit` is set (nothing to signal).\n */\n tokenUsedBps?: number | null;\n /** Tokens left before the aggregate token ceiling. null when no `tokenLimit`. */\n remainingTokens?: number | null;\n /**\n * Per-step headroom for the innermost active step — present (non-null) ONLY\n * while a step() is active AND that dimension is capped. null otherwise.\n */\n stepRemainingUsd?: number | null;\n stepRemainingTokens?: number | null;\n /**\n * Spend rate in USD per minute over this guard's lifetime (spentUsd ÷ minutes\n * since the guard was created) — the number voice teams watch. null only when no\n * wall-clock time has elapsed yet. Make one guard per call/turn for a per-call\n * burn rate; a long-lived guard reports the session average. Optional for the\n * same additive reason as the fields above; `advisory()` always sets it.\n */\n burnRateUsdPerMin?: number | null;\n}\n\nexport class BudgetGuard {\n readonly limitUsd: number;\n spentUsd = 0;\n /**\n * Wall-clock start (ms since epoch) of this guard's spend window, for the $/min\n * burn rate (advisory().burnRateUsdPerMin). One guard per call/turn ⇒ a per-call\n * rate. Set at construction; tests set it directly to control elapsed time.\n */\n createdAtMs = Date.now();\n priceOverrides?: Record<string, ManualPrice>;\n failClosed: boolean;\n nearLimitBps: number;\n /** Aggregate token ceiling, or null when the token dimension is disabled. */\n readonly tokenLimit: number | null;\n /** Aggregate tokens accrued (prompt + completion + cache buckets). */\n spentTokens = 0;\n /** Tokens held in-flight — the token twin of `reserved`. */\n private reservedTokens = 0;\n /** Step stack: innermost step is last. Empty when no step() is active. */\n private readonly steps: StepState[] = [];\n\n private readonly onBlock: (spentUsd: number, limitUsd: number) => void;\n /**\n * Costs of the most recent priced LLM call and tool call, tracked\n * SEPARATELY: the default next-call prediction is the max of the two, so a\n * cheap tool call can't shrink the estimate right before an expensive LLM\n * call (or vice versa) — conservative beats one-call-too-late.\n */\n private lastLlmCost = 0;\n private lastToolCost = 0;\n /** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */\n private reserved = 0;\n /** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */\n private readonly spendEvents: SpendEvent[] = [];\n private readonly maxLogEvents?: number;\n /**\n * Per-tool running totals (settleTool/recordTool) — the tool side of the one\n * shared ceiling, exposed via the toolCosts getter. null-prototype: tool\n * names are caller-supplied strings, so a \"__proto__\" name is stored as\n * plain data instead of mutating the object's prototype.\n */\n private readonly toolCostTotals: Record<string, number> = Object.create(null);\n\n /**\n * Opt-in ledger sync — OFF by default (zero-telemetry). No key is stored and\n * nothing is ever sent until enableSync() is called; see enableSync()/sync().\n */\n private syncEnabled = false;\n private syncKey: string | null = null;\n private syncBase: string | null = null;\n\n /**\n * @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.\n */\n constructor(limitUsd: number, options: BudgetGuardOptions = {}) {\n if (!Number.isFinite(limitUsd) || limitUsd < 0) {\n // NaN/Infinity would make every check() comparison fail-open, silently\n // disabling the guard — reject them up front.\n throw new RangeError(\n `limitUsd must be a finite, non-negative number, got ${limitUsd}`,\n );\n }\n // `=== undefined` (not `??`) so an explicit null is rejected by validation\n // rather than silently defaulting — matches Python, which rejects None.\n const nearLimitBps = options.nearLimitBps === undefined ? 8000 : options.nearLimitBps;\n if (!Number.isInteger(nearLimitBps) || nearLimitBps < 0 || nearLimitBps > 10000) {\n throw new RangeError(\n `nearLimitBps must be an integer in 0..10000, got ${nearLimitBps}`,\n );\n }\n if (\n options.maxLogEvents !== undefined &&\n (!Number.isInteger(options.maxLogEvents) || options.maxLogEvents < 0)\n ) {\n throw new RangeError(\n `maxLogEvents must be a non-negative integer, got ${options.maxLogEvents}`,\n );\n }\n if (\n options.tokenLimit !== undefined &&\n (!Number.isInteger(options.tokenLimit) || options.tokenLimit < 0)\n ) {\n throw new RangeError(\n `tokenLimit must be a non-negative integer, got ${options.tokenLimit}`,\n );\n }\n this.limitUsd = limitUsd;\n this.tokenLimit = options.tokenLimit ?? null;\n this.maxLogEvents = options.maxLogEvents;\n this.priceOverrides = options.priceOverrides;\n this.failClosed = options.failClosed ?? true;\n this.onBlock = options.onBlock ?? defaultOnBlock;\n this.nearLimitBps = nearLimitBps;\n }\n\n /**\n * Throw {@link BudgetExceeded} if the next call would cross the ceiling.\n *\n * Call this immediately before each LLM request. The \"next call\" is estimated\n * conservatively as the costlier of the last LLM call and the last tool call\n * (override with `estimatedNextCost`); the\n * first call is always allowed unless the ceiling is already met. In-flight\n * reservations count toward the total, so this stays correct alongside\n * {@link BudgetGuard.reserve}.\n *\n * Note: `check` is a non-binding peek. For parallel calls, use `reserve()` /\n * `settle()`, which hold the estimate across the await.\n */\n check(estimatedNextCost?: number, options: { estimatedTokens?: number } = {}): void {\n const rawEstimate =\n estimatedNextCost === undefined ? this.defaultEstimate() : estimatedNextCost;\n if (!Number.isFinite(rawEstimate)) {\n // NaN/Infinity would poison the comparisons and fail-open — reject it\n // (parity with the constructor's Number.isFinite guard).\n throw new RangeError(\n `estimatedNextCost must be a finite number, got ${rawEstimate}`,\n );\n }\n const estimate = Math.max(0, rawEstimate);\n this.validateTokenEstimate(options.estimatedTokens);\n const tokens = Math.max(0, options.estimatedTokens ?? 0);\n const blocked = this.blockingCross(estimate, tokens);\n if (blocked !== null) this.raiseBlock(blocked);\n }\n\n /**\n * Atomically check the ceiling AND hold the estimated cost in flight.\n *\n * The concurrency-safe enforcement path: call before the request and hold the\n * returned reservation across the await, so parallel callers can't all clear\n * the same stale total. Throws {@link BudgetExceeded} (without reserving) if\n * the reservation would cross the ceiling. Returns the reservation handle to\n * pass to {@link BudgetGuard.settle} (or {@link BudgetGuard.release} on error).\n * `estimatedCost` defaults to the costlier of the last LLM call and the last\n * tool call.\n */\n reserve(estimatedCost?: number, options: { estimatedTokens?: number } = {}): ReservationHandle {\n const rawEstimate = estimatedCost === undefined ? this.defaultEstimate() : estimatedCost;\n if (!Number.isFinite(rawEstimate)) {\n // NaN would poison this.reserved and fail-open the ceiling — reject it.\n throw new RangeError(\n `estimatedCost must be a finite number, got ${rawEstimate}`,\n );\n }\n const estimate = Math.max(0, rawEstimate);\n // Reject a fractional/NaN/Infinity token estimate BEFORE mutating\n // reservedTokens/step holds — a NaN would fail-open the integer token\n // comparisons and corrupt the tally. A negative int is clamped by Math.max.\n this.validateTokenEstimate(options.estimatedTokens);\n const tokens = Math.max(0, options.estimatedTokens ?? 0);\n const blocked = this.blockingCross(estimate, tokens);\n if (blocked !== null) this.raiseBlock(blocked);\n this.reserved += estimate;\n this.reservedTokens += tokens;\n const step = this.steps.length ? this.steps[this.steps.length - 1] : null;\n if (step !== null) {\n step.reservedUsd += estimate;\n step.reservedTokens += tokens;\n }\n // Plain number only when nothing token- or step-shaped is in play, so\n // pre-existing USD-only callers get byte-for-byte the old handle.\n if (tokens === 0 && step === null) return estimate;\n return { usd: estimate, tokens };\n }\n\n /**\n * Release a reservation and record the actual cost. `record` is `settle` with\n * no reservation. Returns the USD cost of this call; unpriceable-model handling\n * matches {@link BudgetGuard.record}, and any held reservation is released even\n * on the warn-and-skip path. A priced call appends one {@link SpendEvent} to\n * {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);\n * the warn-and-skip path accrues nothing and logs nothing, so the ledger stays\n * in lockstep with `spentUsd`.\n */\n settle(\n model: string,\n promptTokens: number,\n completionTokens: number,\n options: { reserved?: ReservationHandle; price?: ManualPrice; label?: string } = {},\n ): number {\n const reserved = options.reserved ?? 0;\n // A bad reserved handle would corrupt this.reserved and break the ceiling for\n // OTHER in-flight calls (negative → phantom hold; Infinity → clears all holds).\n // reservedUsdOf validates the handle (raw number, or a BudgetReservation's fields).\n const reservedUsd = this.reservedUsdOf(reserved);\n let overrides = this.priceOverrides;\n if (options.price !== undefined) {\n overrides = { ...(overrides ?? {}), [model]: options.price };\n }\n\n const priced = resolvePrice(model, overrides);\n if (priced === null) {\n console.warn(\n `Cannot price model '${model}': not in the bundled cost map and no ` +\n `manual price given. The budget guard cannot enforce a ceiling on ` +\n `spend it cannot measure — pass { price } or set it in priceOverrides.`,\n );\n // Release any held reservation on BOTH paths. Fail-closed must not leak\n // the in-flight hold, or reserved grows permanently and remainingUsd\n // shrinks until reserve() starts blocking everything.\n this.release(reserved);\n if (this.failClosed) {\n throw new UnpriceableModelError(model);\n }\n return 0;\n }\n\n let cost: number;\n try {\n cost = priceTokens(priced, promptTokens, completionTokens);\n } catch (err) {\n // priceTokens can throw (e.g. non-finite costs). Release the in-flight\n // hold before re-throwing so `reserved` doesn't leak and shrink\n // remainingUsd permanently — same fail-safe as the unpriceable path above.\n this.release(reserved);\n throw err;\n }\n if (reserved) {\n this.consumeReservation(reserved);\n }\n // Tokens accrued match what priceTokens bills (prompt + completion; negative\n // counts clamp to 0, as in pricing).\n const accruedTokens = Math.max(0, promptTokens) + Math.max(0, completionTokens);\n this.spentUsd += cost;\n this.spentTokens += accruedTokens;\n this.accrueStep(cost, accruedTokens);\n // Clamp a sub-epsilon float overshoot back to the limit so the running total\n // never reports as having crossed the ceiling by a rounding artifact.\n if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {\n this.spentUsd = this.limitUsd;\n }\n this.lastLlmCost = cost;\n this.appendEvent({\n timestamp: Date.now() / 1000,\n kind: \"llm\",\n modelOrTool: model,\n promptTokens,\n completionTokens,\n costUsd: cost,\n ...(options.label !== undefined ? { label: options.label } : {}),\n // 0 means \"no reservation\" (the plain record() path) — omit rather than\n // log a meaningless zero. Log the USD amount so the ledger schema stays\n // number-shaped even for a BudgetReservation.\n ...(reservedUsd ? { reserved: reservedUsd } : {}),\n });\n return cost;\n }\n\n /**\n * Price one response's tokens offline and add the cost to the total.\n *\n * Returns the USD cost of this call. If the model is unpriceable and no `price`\n * is given, behaviour depends on `failClosed`: warn + throw (default), or\n * warn + skip accrual.\n */\n record(\n model: string,\n promptTokens: number,\n completionTokens: number,\n options: { price?: ManualPrice; label?: string } = {},\n ): number {\n return this.settle(model, promptTokens, completionTokens, {\n reserved: 0,\n price: options.price,\n label: options.label,\n });\n }\n\n /**\n * Atomically check the ceiling AND hold a tool call's cost in flight.\n *\n * The tool-spend counterpart of {@link BudgetGuard.reserve} — and STRONGER\n * than the LLM path, because a paid tool's price is usually known exactly\n * before the call, so the pre-call hard-stop is precise rather than\n * estimated:\n *\n * const handle = guard.reserveTool(0.02); // throws BEFORE Apollo runs\n * const result = await apollo.peopleLookup(...);\n * guard.settleTool(\"apollo.people_lookup\", 0.02, { reserved: handle });\n *\n * Throws {@link BudgetExceeded} (without reserving) if the call would cross\n * the ceiling. The estimate is required — tools have no last-cost prediction\n * worth falling back to. Pass the returned handle to\n * {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.\n */\n reserveTool(estimatedCost: number): ReservationHandle {\n if (estimatedCost === undefined) {\n // reserve(undefined) would silently fall back to the last-cost prediction\n // (0 on a fresh guard) — an unguarded tool call. A missing price must\n // fail loudly, e.g. guard.reserveTool(priceTable[tool]).\n throw new RangeError(\"reserveTool requires an estimated cost, got undefined\");\n }\n if (!Number.isFinite(estimatedCost) || estimatedCost < 0) {\n // reserve() clamps a negative estimate to 0 (lenient LLM contract) — for\n // a tool that would reserve nothing: the same unguarded call.\n throw new RangeError(\n `estimatedCost must be a finite, non-negative number, got ${estimatedCost}`,\n );\n }\n return this.reserve(estimatedCost);\n }\n\n /**\n * Release a reservation and record a tool call's actual cost.\n *\n * `recordTool` is `settleTool` with no reservation. The caller supplies the\n * cost — tools have no token usage to price. Accrues into the same\n * `spentUsd` ceiling as tokens, tallies the per-tool total\n * ({@link BudgetGuard.toolCosts}), updates the tool side of the next-call\n * estimate (tracked separately from the LLM side; the default prediction is\n * the max of the two, so a tool-hammering loop's plain `check()` stops\n * BEFORE the crossing call without a cheap tool shrinking the LLM\n * prediction), and appends\n * a `kind: \"tool\"` {@link SpendEvent} to {@link BudgetGuard.spendLog}.\n * Returns `costUsd`.\n */\n settleTool(\n tool: string,\n costUsd: number,\n options: { reserved?: ReservationHandle; label?: string } = {},\n ): number {\n if (!Number.isFinite(costUsd) || costUsd < 0) {\n throw new RangeError(`costUsd must be a finite, non-negative number, got ${costUsd}`);\n }\n const reserved = options.reserved ?? 0;\n // A bad reserved handle would corrupt the in-flight tally and break the\n // ceiling for OTHER calls — same contract as settle(). reservedUsdOf\n // validates the handle (raw number, or a BudgetReservation's fields).\n const reservedUsd = this.reservedUsdOf(reserved);\n if (reserved) {\n this.consumeReservation(reserved);\n }\n this.spentUsd += costUsd;\n // A paid tool spends the step's USD too (no tokens to price).\n this.accrueStep(costUsd, 0);\n // Same sub-epsilon clamp as settle(): never report a rounding-artifact\n // crossing of the ceiling.\n if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {\n this.spentUsd = this.limitUsd;\n }\n this.lastToolCost = costUsd;\n this.toolCostTotals[tool] = (this.toolCostTotals[tool] ?? 0) + costUsd;\n this.appendEvent({\n timestamp: Date.now() / 1000,\n kind: \"tool\",\n modelOrTool: tool,\n promptTokens: null,\n completionTokens: null,\n costUsd,\n ...(options.label !== undefined ? { label: options.label } : {}),\n ...(reservedUsd ? { reserved: reservedUsd } : {}),\n });\n return costUsd;\n }\n\n /**\n * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.\n *\n * Post-hoc accrual for costs only known after the call (metered APIs); when\n * the price is known up front, {@link BudgetGuard.reserveTool} /\n * {@link BudgetGuard.settleTool} give the stronger pre-call hard-stop. See\n * `settleTool` for the full contract. Returns `costUsd`.\n */\n recordTool(tool: string, costUsd: number, options: { label?: string } = {}): number {\n return this.settleTool(tool, costUsd, { reserved: 0, label: options.label });\n }\n\n /**\n * Drop an in-flight reservation without recording spend (e.g. the call failed\n * before producing usage). Safe to call with `0`.\n */\n release(reserved: ReservationHandle): void {\n // Validate before the zero-check so a NaN handle throws instead of being\n // silently dropped (a leak); a bad handle corrupts the in-flight tally.\n // reservedUsdOf validates the handle (raw number, or a BudgetReservation's fields).\n this.reservedUsdOf(reserved);\n if (!reserved) return;\n this.consumeReservation(reserved);\n }\n\n /** USD left before the ceiling, net of in-flight reservations (never negative). */\n get remainingUsd(): number {\n return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);\n }\n\n /**\n * Per-tool running USD totals, keyed by the name given to `settleTool()` /\n * `recordTool()` — e.g. `{\"apollo.people_lookup\": 0.42, \"exa.search\": 0.11}`.\n * Makes the token/tool split of the one shared ceiling inspectable\n * (`spentUsd - sum of toolCosts` is the token side). Returns a snapshot copy.\n */\n get toolCosts(): Record<string, number> {\n return { ...this.toolCostTotals };\n }\n\n /**\n * The per-call spend ledger, oldest first — one {@link SpendEvent} per priced\n * `record()` / `settle()` / `recordTool()` / `settleTool()`. Returns a\n * snapshot copy: mutating it cannot corrupt the ledger.\n */\n get spendLog(): SpendEvent[] {\n return [...this.spendEvents];\n }\n\n /**\n * The spend ledger as JSONL — one event per line, newline-terminated.\n *\n * The schema is stable and language-independent (snake_case keys, fixed order;\n * optional fields omitted when absent), identical to the Python package's\n * `export_log()`, so heterogeneous agents produce logs you can concatenate and\n * analyse as one stream. (The *schema* is the contract, not the bytes: the two\n * runtimes may render the same float differently, e.g. JS `0.0000025` vs\n * Python `2.5e-06`.) Empty ledger yields `\"\"`.\n */\n exportLog(): string {\n return this.spendEvents\n .map((e) => {\n // snake_case wire shape, fixed key order — the cross-language schema.\n const row: Record<string, unknown> = {\n timestamp: e.timestamp,\n kind: e.kind,\n model_or_tool: e.modelOrTool,\n prompt_tokens: e.promptTokens,\n completion_tokens: e.completionTokens,\n cost_usd: e.costUsd,\n };\n if (e.label !== undefined) row.label = e.label;\n if (e.reserved !== undefined) row.reserved = e.reserved;\n return `${JSON.stringify(row)}\\n`;\n })\n .join(\"\");\n }\n\n /**\n * **Opt in** to pushing this guard's ledger to Floe's Reconcile Mode.\n *\n * Off by default — this is the *only* way to turn sync on, and it turns on\n * nothing else: it stores your key/endpoint and does **not** send. Nothing\n * leaves the process until you call {@link BudgetGuard.sync}. Zero-telemetry\n * stays the default; there is no background send.\n *\n * Why: Floe's gateway can't see spend it never routed (BYOK, self-hosted,\n * off-path). Syncing your local ledger is what makes that spend — and your\n * **Coverage Score** — visible. Budget, not balance: it reports what you already\n * spent for coverage/attribution; it moves no money.\n *\n * What will leave the process when you call {@link BudgetGuard.sync}: exactly the\n * {@link BudgetGuard.exportLog} JSONL (priced spend events — no prompts, no\n * content, no identifiers beyond a `label` you set).\n *\n * @param apiKey Floe agent key (`floe_<hex>`, `read_write`). Defaults to the\n * `FLOE_API_KEY` env var at `sync()` time.\n * @param options.baseUrl API base. Defaults to `FLOE_API_BASE_URL`, else prod.\n *\n * Revoke with {@link BudgetGuard.disableSync}; the guard sends nothing after.\n */\n enableSync(apiKey?: string, options: { baseUrl?: string } = {}): void {\n this.syncEnabled = true;\n this.syncKey = apiKey ?? null;\n this.syncBase = options.baseUrl ?? null;\n }\n\n /**\n * Turn ledger sync back off (revoke the opt-in). The guard sends nothing after\n * this; {@link BudgetGuard.sync} throws until {@link BudgetGuard.enableSync} is\n * called again.\n */\n disableSync(): void {\n this.syncEnabled = false;\n this.syncKey = null;\n this.syncBase = null;\n }\n\n /**\n * Push the current spend ledger to Reconcile Mode; resolve with events accepted.\n *\n * Requires {@link BudgetGuard.enableSync} first — this is the explicit send, the\n * only thing that transmits the ledger. Throws if sync isn't enabled (so a guard\n * that never opted in can never send). An empty ledger is a no-op (`0`, no\n * network). Re-syncing is safe: the server is idempotent, so already-ingested\n * events are not double-counted.\n *\n * @throws {Error} sync was not enabled (call {@link BudgetGuard.enableSync} first).\n * @throws {LedgerSyncError} the opt-in send failed (missing key, non-2xx, network).\n */\n async sync(): Promise<number> {\n // Snapshot opt-in state + key/base synchronously, before any await: a\n // disableSync() interleaved on a later microtask can't clear the key mid-call\n // (which would silently fall back to $FLOE_API_KEY) or let an upload start\n // after revocation. A disable happens-before (we throw) or happens-after (we\n // send the key we captured while still enabled).\n const enabled = this.syncEnabled;\n const key = this.syncKey;\n const base = this.syncBase;\n if (!enabled) {\n throw new Error(\n \"ledger sync is not enabled — call guard.enableSync(apiKey) first. \" +\n \"Sync is opt-in and off by default (zero-telemetry).\",\n );\n }\n return pushLedger(this.exportLog(), key ?? undefined, { baseUrl: base ?? undefined });\n }\n\n /**\n * The default next-call prediction when the caller supplies no estimate.\n * Conservative: the costlier of the last LLM call and the last tool call — a\n * mixed loop predicts the pricier kind, which at worst blocks one call early\n * (fail-closed) rather than letting a crossing call through because the LAST\n * event happened to be cheap.\n */\n private defaultEstimate(): number {\n return Math.max(this.lastLlmCost, this.lastToolCost);\n }\n\n /**\n * Subtract a settled/released hold from the in-flight tally. A handle larger\n * than EVERYTHING currently held cannot have come from a matching\n * `reserve()` — throwing beats silently clamping, which would free OTHER\n * callers' holds and fail the ceiling open. The epsilon absorbs float dust\n * from accumulating and draining many holds; per-caller over-release (a\n * handle within the total but larger than the caller's own hold) is\n * undetectable without per-handle tracking and remains the caller's\n * responsibility.\n */\n private consumeReservation(reserved: ReservationHandle): void {\n const usd = isReservation(reserved) ? reserved.usd : reserved;\n const tokens = isReservation(reserved) ? reserved.tokens : 0;\n if (usd > this.reserved + EPS) {\n throw new RangeError(\n `reserved handle (${usd}) exceeds total in-flight reservations ` +\n `(${this.reserved}) — a handle must come from a matching reserve()`,\n );\n }\n if (tokens > this.reservedTokens) {\n throw new RangeError(\n `reserved token handle (${tokens}) exceeds total in-flight token ` +\n `reservations (${this.reservedTokens}) — a handle must come from a matching reserve()`,\n );\n }\n this.reserved = Math.max(0, this.reserved - usd);\n this.reservedTokens = Math.max(0, this.reservedTokens - tokens);\n // Drain the innermost active step too (mirrors the aggregate drain). A\n // handle is drained against whatever step is innermost now — the\n // sequential-loop contract; concurrent parallel steps are out of scope (#46).\n const step = this.steps.length ? this.steps[this.steps.length - 1] : null;\n if (step !== null) {\n step.reservedUsd = Math.max(0, step.reservedUsd - usd);\n step.reservedTokens = Math.max(0, step.reservedTokens - tokens);\n }\n }\n\n /**\n * The USD amount of a handle, validated. Both shapes are checked — a raw\n * number and a {@link BudgetReservation}'s `usd`/`tokens` fields — so a bad\n * hand-rolled handle can't corrupt the in-flight tally.\n */\n /**\n * Validate a caller-supplied token estimate. Rejects a fraction, NaN,\n * Infinity, or boolean (via `Number.isInteger`) so it can't disable the\n * integer token hard-stops or corrupt the in-flight token tally. `undefined`\n * (the default) is fine; a negative integer is clamped by `Math.max(0, ...)`,\n * matching the lenient USD estimate. Mirrors Python's `_validate_token_estimate`.\n */\n private validateTokenEstimate(estimatedTokens: number | undefined): void {\n if (estimatedTokens !== undefined && !Number.isInteger(estimatedTokens)) {\n throw new RangeError(`estimatedTokens must be an integer, got ${estimatedTokens}`);\n }\n }\n\n private reservedUsdOf(reserved: ReservationHandle): number {\n if (isReservation(reserved)) {\n // A BudgetReservation is public (re-exported), so validate its fields here\n // — a hand-rolled { usd: NaN, tokens: -1 } must not slip past into\n // consumeReservation and corrupt the in-flight tallies. Mirrors Python's\n // BudgetReservation.__post_init__.\n if (!Number.isFinite(reserved.usd) || reserved.usd < 0) {\n throw new RangeError(\n `reserved.usd must be a finite, non-negative number, got ${reserved.usd}`,\n );\n }\n if (!Number.isInteger(reserved.tokens) || reserved.tokens < 0) {\n throw new RangeError(\n `reserved.tokens must be a non-negative integer, got ${reserved.tokens}`,\n );\n }\n return reserved.usd;\n }\n if (!Number.isFinite(reserved) || reserved < 0) {\n throw new RangeError(`reserved must be a finite, non-negative number, got ${reserved}`);\n }\n return reserved;\n }\n\n /**\n * Accrue a settled call into the innermost active step (no-op when none).\n * Sequential-loop contract: the innermost step owns the call.\n */\n private accrueStep(cost: number, tokens: number): void {\n const step = this.steps.length ? this.steps[this.steps.length - 1] : null;\n if (step !== null) {\n step.spentUsd += cost;\n step.spentTokens += tokens;\n }\n }\n\n /**\n * The ONE choke point, across two dimensions and two scopes. Returns the first\n * ceiling that blocks as `[dimension, scope]` — dimension `\"usd\" | \"tokens\"`,\n * scope `\"aggregate\" | \"step\"` — or `null` if the call fits everywhere.\n * Aggregate is checked before step (the guard-wide ceiling is the hard limit).\n */\n private blockingCross(\n estimateUsd: number,\n estimateTokens: number,\n ): [\"usd\" | \"tokens\", \"aggregate\" | \"step\"] | null {\n // Aggregate USD — same comparison the original check/reserve used.\n const committed = this.spentUsd + this.reserved;\n if (committed > this.limitUsd - EPS || committed + estimateUsd > this.limitUsd + EPS) {\n return [\"usd\", \"aggregate\"];\n }\n // Aggregate tokens (integers — no epsilon).\n if (this.tokenLimit !== null) {\n const committedT = this.spentTokens + this.reservedTokens;\n if (committedT >= this.tokenLimit || committedT + estimateTokens > this.tokenLimit) {\n return [\"tokens\", \"aggregate\"];\n }\n }\n // Innermost active step's caps (crossing an outer step requires first\n // crossing the inner one, so the innermost is sufficient).\n const step = this.steps.length ? this.steps[this.steps.length - 1] : null;\n if (step !== null) {\n if (step.maxUsd !== null) {\n const sCommitted = step.spentUsd + step.reservedUsd;\n if (sCommitted > step.maxUsd - EPS || sCommitted + estimateUsd > step.maxUsd + EPS) {\n return [\"usd\", \"step\"];\n }\n }\n if (step.maxTokens !== null) {\n const sCommittedT = step.spentTokens + step.reservedTokens;\n if (sCommittedT >= step.maxTokens || sCommittedT + estimateTokens > step.maxTokens) {\n return [\"tokens\", \"step\"];\n }\n }\n }\n return null;\n }\n\n /** Notify + throw the right error for a [dimension, scope] block. */\n private raiseBlock(blocked: [\"usd\" | \"tokens\", \"aggregate\" | \"step\"]): never {\n const [dimension, scope] = blocked;\n if (dimension === \"usd\") {\n this.onBlock(this.spentUsd, this.limitUsd);\n throw new BudgetExceeded(this.spentUsd, this.limitUsd);\n }\n let spentT: number;\n let limitT: number;\n if (scope === \"step\") {\n const step = this.steps[this.steps.length - 1];\n spentT = step.spentTokens + step.reservedTokens;\n limitT = step.maxTokens ?? 0;\n } else {\n spentT = this.spentTokens + this.reservedTokens;\n limitT = this.tokenLimit ?? 0;\n }\n throw new TokenBudgetExceeded(spentT, limitT, scope);\n }\n\n /**\n * Scope a per-step USD and/or token cap for a **sequential agent loop**.\n *\n * Runs `fn` with a step pushed onto the guard's stack; the\n * enforcement/accrual path honours the innermost active step *on top of* the\n * aggregate ceilings. A call that would cross the step's `maxUsd` / `maxTokens`\n * is hard-blocked ({@link BudgetExceeded} / {@link TokenBudgetExceeded} with\n * `scope: \"step\"`) even if the aggregate budget has room. `fn` receives the\n * SAME guard, so no adapter needs to know about steps. The step is popped when\n * `fn` settles or throws. **Not for concurrent parallel steps on one guard** —\n * that's out of scope for issue #46; use one guard per parallel branch.\n */\n step<T>(\n options: { maxUsd?: number; maxTokens?: number },\n fn: (guard: BudgetGuard) => T,\n ): T {\n const { maxUsd, maxTokens } = options;\n if (maxUsd !== undefined && (!Number.isFinite(maxUsd) || maxUsd < 0)) {\n throw new RangeError(`maxUsd must be a finite, non-negative number, got ${maxUsd}`);\n }\n if (maxTokens !== undefined && (!Number.isInteger(maxTokens) || maxTokens < 0)) {\n throw new RangeError(`maxTokens must be a non-negative integer, got ${maxTokens}`);\n }\n const state: StepState = {\n maxUsd: maxUsd ?? null,\n maxTokens: maxTokens ?? null,\n spentUsd: 0,\n spentTokens: 0,\n reservedUsd: 0,\n reservedTokens: 0,\n };\n this.steps.push(state);\n // Pop our own state; splice by identity so an out-of-order exit can't\n // silently drop someone else's step.\n const pop = (): void => {\n const idx = this.steps.lastIndexOf(state);\n if (idx !== -1) this.steps.splice(idx, 1);\n };\n let result: T;\n try {\n result = fn(this);\n } catch (err) {\n pop();\n throw err;\n }\n // An async `fn` returns a promise synchronously — popping now would disable\n // step enforcement across every `await` inside it (the common agent-loop\n // shape). Keep the step on the stack until the promise settles, then pop.\n if (result != null && typeof (result as { then?: unknown }).then === \"function\") {\n return (result as unknown as Promise<unknown>).finally(pop) as unknown as T;\n }\n pop();\n return result;\n }\n\n private appendEvent(event: SpendEvent): void {\n // Frozen for parity with Python's frozen dataclass: spendLog copies the\n // array but shares the event objects, so an unfrozen event would let a\n // consumer silently rewrite logged history.\n this.spendEvents.push(Object.freeze(event));\n if (this.maxLogEvents !== undefined && this.spendEvents.length > this.maxLogEvents) {\n // Ring buffer: drop the oldest overflow (at most one per append).\n this.spendEvents.splice(0, this.spendEvents.length - this.maxLogEvents);\n }\n }\n\n /**\n * Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.\n *\n * `nearLimit` flips once utilization reaches `nearLimitBps` (default 80%), so an\n * agent can taper *before* the hard-stop. Advisory only: read it to adapt;\n * {@link BudgetGuard.check} is what enforces the ceiling.\n */\n advisory(): BudgetAdvisory {\n // Floor (not round) so usedBps never over-reports utilization and nearLimit\n // flips exactly when the threshold is reached; the epsilon absorbs float noise\n // and Math.floor matches Python's int() exactly (round() would diverge).\n const usedBps =\n this.limitUsd <= 0\n ? 10000\n : Math.max(0, Math.min(10000, Math.floor((this.spentUsd / this.limitUsd) * 10000 + 1e-9)));\n const remainingUsd = Math.max(0, this.limitUsd - this.spentUsd);\n // The costlier of the last LLM and tool call — the guard's own default\n // reservation estimate; 0 before any call is recorded.\n const expectedCost = Math.max(this.lastLlmCost, this.lastToolCost);\n // +1e-9 absorbs float noise so e.g. 0.6/0.2 floors to 3 not 2 (same epsilon\n // rationale as usedBps above, and keeps Python int() parity).\n const estCallsRemaining =\n expectedCost > 0 ? Math.floor(remainingUsd / expectedCost + 1e-9) : null;\n // Burn rate: spend ÷ minutes elapsed since the guard was created. null until\n // any wall-clock time has passed (avoids a divide-by-zero at t0); $0 spent over\n // real elapsed time is a legitimate 0/min, not null.\n const elapsedMin = (Date.now() - this.createdAtMs) / 60000;\n const burnRateUsdPerMin = elapsedMin > 0 ? this.spentUsd / elapsedMin : null;\n // Aggregate token utilization, mirroring usedBps. null (no signal) when the\n // token dimension is unset — a pure USD guard's advisory is unchanged.\n let tokenUsedBps: number | null = null;\n let remainingTokens: number | null = null;\n let nearToken = false;\n if (this.tokenLimit !== null) {\n tokenUsedBps =\n this.tokenLimit <= 0\n ? 10000\n : Math.max(\n 0,\n Math.min(10000, Math.floor((this.spentTokens / this.tokenLimit) * 10000 + 1e-9)),\n );\n remainingTokens = Math.max(0, this.tokenLimit - this.spentTokens);\n nearToken = tokenUsedBps >= this.nearLimitBps;\n }\n // Innermost active step's headroom + nearness (per dimension, only when\n // capped). null keeps the fields absent for old readers.\n const step = this.steps.length ? this.steps[this.steps.length - 1] : null;\n let stepRemainingUsd: number | null = null;\n let stepRemainingTokens: number | null = null;\n let nearStep = false;\n if (step !== null) {\n if (step.maxUsd !== null) {\n stepRemainingUsd = Math.max(0, step.maxUsd - step.spentUsd);\n if (step.maxUsd <= 0) {\n nearStep = true;\n } else {\n const sBps = Math.floor((step.spentUsd / step.maxUsd) * 10000 + 1e-9);\n nearStep = nearStep || sBps >= this.nearLimitBps;\n }\n }\n if (step.maxTokens !== null) {\n stepRemainingTokens = Math.max(0, step.maxTokens - step.spentTokens);\n if (step.maxTokens <= 0) {\n nearStep = true;\n } else {\n const sTBps = Math.floor((step.spentTokens / step.maxTokens) * 10000 + 1e-9);\n nearStep = nearStep || sTBps >= this.nearLimitBps;\n }\n }\n }\n return {\n // nearLimit now also flips when a token ceiling or the active step is near\n // its cap, so a router can downshift before ANY hard-stop.\n nearLimit: usedBps >= this.nearLimitBps || nearToken || nearStep,\n usedBps,\n // Settled budget: limit minus accrued spend, deliberately NOT net of\n // in-flight reservations. Unlike the remainingUsd getter (which subtracts\n // `reserved`), the advisory is a soft utilization signal about money already\n // spent, while the getter reports what a new call can still claim.\n remainingUsd,\n limitUsd: this.limitUsd,\n spentUsd: this.spentUsd,\n scope: \"local\",\n expectedCost,\n estCallsRemaining,\n tokenUsedBps,\n remainingTokens,\n stepRemainingUsd,\n stepRemainingTokens,\n burnRateUsdPerMin,\n };\n }\n}\n\nfunction defaultOnBlock(spentUsd: number, limitUsd: number): void {\n console.error(\n \"BUDGET EXCEEDED — call blocked\\n\" +\n ` spent so far: $${spentUsd.toFixed(6)} | ceiling: $${limitUsd.toFixed(6)}\\n` +\n \" The next call would cross your budget; floe-guard stopped your agent \" +\n \"before it ran.\",\n );\n}\n","/**\n * LatencyBudget — a cumulative tool-chain deadline, sibling to BudgetGuard.\n *\n * BudgetGuard stops an agent before its next call crosses a USD ceiling;\n * LatencyBudget stops it before the next call would blow an end-user SLA:\n *\n * ```ts\n * const deadline = new LatencyBudget(5000);\n * ...\n * deadline.check(800); // throws DeadlineExceeded when projected over\n * if (deadline.advisory().nearDeadline) useFasterModel();\n * router.pick({ maxLatencyMs: deadline.remainingMs });\n * ```\n *\n * Design notes (mirroring `src/floe_guard/latency.py`):\n * - **Monotonic clock** — `performance.now()`, never wall time.\n * - **Cooperative, not preemptive** — the guard provides the deadline signal;\n * aborting a stalled in-flight call is the framework's job (AbortSignal).\n * `check()` prevents the NEXT call from starting.\n * - **Advisory symmetry** — `nearDeadline` / `usedBps` / `remainingMs` are the\n * latency twin of BudgetGuard's `nearLimit` / `usedBps` / `remainingUsd`.\n * - **In-process scope** — one instance per request/run; distributed latency\n * tracking is out of scope.\n */\n\nimport { DeadlineExceeded } from \"./errors.js\";\n\n/** A context-aware deadline signal — the latency twin of {@link BudgetAdvisory}.\n * Soft by design; the hard-stop is {@link LatencyBudget.check}. */\nexport interface LatencyAdvisory {\n nearDeadline: boolean;\n /** SLA consumed, basis points 0..10000 (8500 = 85%). */\n usedBps: number;\n remainingMs: number;\n slaMs: number;\n elapsedMs: number;\n}\n\nexport interface LatencyBudgetOptions {\n /**\n * Utilization (basis points, 0..10000) at which {@link LatencyBudget.advisory}\n * flags `nearDeadline` so an agent can downshift to a faster path before the\n * wall. Default 8000 (80%), matching BudgetGuard's `nearLimitBps`.\n */\n nearDeadlineBps?: number;\n /** Invoked with `(elapsedMs, slaMs)` right before {@link DeadlineExceeded} is thrown. */\n onBlock?: (elapsedMs: number, slaMs: number) => void;\n /** Milliseconds-returning monotonic clock, injectable for tests. Defaults to `performance.now`. */\n clock?: () => number;\n}\n\nexport class LatencyBudget {\n readonly slaMs: number;\n readonly nearDeadlineBps: number;\n private readonly onBlock?: (elapsedMs: number, slaMs: number) => void;\n private readonly clock: () => number;\n private readonly startedAt: number;\n\n /** The budget starts counting at construction — build it when the request\n * (and its SLA) starts. */\n constructor(slaMs: number, options: LatencyBudgetOptions = {}) {\n if (!(Number.isFinite(slaMs) && slaMs > 0)) {\n throw new RangeError(\"slaMs must be > 0\");\n }\n const nearDeadlineBps = options.nearDeadlineBps ?? 8000;\n if (!Number.isInteger(nearDeadlineBps) || nearDeadlineBps < 0 || nearDeadlineBps > 10000) {\n throw new RangeError(\"nearDeadlineBps must be an integer 0..10000\");\n }\n this.slaMs = slaMs;\n this.nearDeadlineBps = nearDeadlineBps;\n this.onBlock = options.onBlock;\n this.clock = options.clock ?? (() => performance.now());\n this.startedAt = this.clock();\n }\n\n /** Milliseconds since construction (monotonic). */\n get elapsedMs(): number {\n return this.clock() - this.startedAt;\n }\n\n /** Milliseconds left before the SLA, floored at 0 — the readable signal a\n * router uses to pick a faster fallback or truncate work mid-chain. */\n get remainingMs(): number {\n return Math.max(0, this.slaMs - this.elapsedMs);\n }\n\n /**\n * Throw {@link DeadlineExceeded} when the projected elapsed time (now +\n * `expectedMs` for the upcoming call) would blow the SLA. Call it\n * immediately before each tool/model call; pass 0 to only gate on time\n * already spent.\n */\n check(expectedMs = 0): void {\n if (!(Number.isFinite(expectedMs) && expectedMs >= 0)) {\n throw new RangeError(\"expectedMs must be >= 0\");\n }\n const elapsed = this.elapsedMs;\n if (elapsed + expectedMs > this.slaMs) {\n this.onBlock?.(elapsed, this.slaMs);\n throw new DeadlineExceeded(elapsed, this.slaMs);\n }\n }\n\n /** The soft near-deadline signal — symmetric to `BudgetGuard.advisory()`. */\n advisory(): LatencyAdvisory {\n const elapsed = this.elapsedMs;\n // round (not floor) — matches the Python twin; float clock arithmetic can\n // land a hair under an exact boundary.\n const usedBps = elapsed > 0 ? Math.min(10000, Math.round((elapsed * 10000) / this.slaMs)) : 0;\n return {\n nearDeadline: usedBps >= this.nearDeadlineBps,\n usedBps,\n remainingMs: Math.max(0, this.slaMs - elapsed),\n slaMs: this.slaMs,\n elapsedMs: elapsed,\n };\n }\n}\n","/**\n * Vercel AI SDK middleware that enforces a {@link BudgetGuard} in the call path.\n *\n * This is the TypeScript counterpart to the Python framework adapters. The AI SDK\n * is TypeScript-only, so it ships as its own npm package.\n *\n * Works with BOTH `ai@4` (`LanguageModelV1Middleware`) and `ai@5`\n * (`LanguageModelV2Middleware`). The two majors renamed the middleware type and\n * the usage fields (`promptTokens`/`completionTokens` → `inputTokens`/\n * `outputTokens`), so this module deliberately imports nothing from `ai`: the\n * middleware is typed structurally against the surface both majors share, and\n * usage is read from whichever field pair the installed SDK reports.\n *\n * - `wrapGenerate({ doGenerate, model })` — we `reserve()` (throws to hard-stop)\n * BEFORE calling `doGenerate()`, hold the reservation across the await, then\n * `settle()` from `result.usage`.\n * - `wrapStream({ doStream, model })` — we `reserve()` BEFORE `doStream()`, then\n * `settle()` from the `finish` part as the stream drains.\n *\n * Reserving before the await is what makes parallel calls (`Promise.all` over\n * several generations) honour the ceiling: each holds its slice instead of all\n * reading the same stale total (issue #18). The reservation is released if the\n * call throws, or if a stream ends without reporting usage.\n *\n * The model id used for pricing comes from `model.modelId`.\n */\n\nimport type { BudgetGuard } from \"./guard.js\";\n\n/**\n * The middleware call surface shared by `ai@4` and `ai@5`. Both majors invoke\n * `wrapGenerate`/`wrapStream` with an options object carrying these members\n * (plus richer `model` fields we don't read). `doGenerate`/`doStream` are\n * declared optional so every 4.x/5.x minor's options type stays assignable —\n * the SDK always provides the one each hook actually calls.\n */\ninterface MiddlewareCallOptions {\n doGenerate?: () => PromiseLike<any>;\n doStream?: () => PromiseLike<any>;\n model: { modelId: string };\n params?: unknown;\n}\n\n/**\n * Structural stand-in for `LanguageModelV1Middleware` (ai@4) and\n * `LanguageModelV2Middleware` (ai@5) — assignable to the `middleware` option of\n * `wrapLanguageModel` on either major.\n */\nexport interface BudgetGuardMiddleware {\n wrapGenerate: (options: MiddlewareCallOptions) => Promise<any>;\n wrapStream: (options: MiddlewareCallOptions) => Promise<any>;\n}\n\n/**\n * Read prompt/completion token counts from an ai@4 usage object\n * (`promptTokens`/`completionTokens`) or an ai@5 one (`inputTokens`/\n * `outputTokens`). Throws when either count is missing or non-numeric: the guard\n * cannot meter spend it cannot see, and treating it as $0 would fail open.\n */\nfunction usageTokens(\n modelId: string,\n usage: unknown,\n): { promptTokens: number; completionTokens: number } {\n const u = usage as\n | {\n promptTokens?: unknown;\n completionTokens?: unknown;\n inputTokens?: unknown;\n outputTokens?: unknown;\n }\n | null\n | undefined;\n const promptTokens = u?.promptTokens ?? u?.inputTokens;\n const completionTokens = u?.completionTokens ?? u?.outputTokens;\n if (typeof promptTokens !== \"number\" || typeof completionTokens !== \"number\") {\n throw new Error(\n `Model '${modelId}' reported no token usage — the budget guard cannot ` +\n `meter spend it cannot see, so this call is rejected rather than ` +\n `treated as free.`,\n );\n }\n return { promptTokens, completionTokens };\n}\n\n/**\n * Build a budget-guard middleware that hard-stops the model before a call\n * crosses the guard's USD ceiling, and records priced token usage after.\n * Compatible with `wrapLanguageModel` from both `ai@4` and `ai@5`.\n *\n * @example\n * import { wrapLanguageModel } from \"ai\";\n * import { openai } from \"@ai-sdk/openai\";\n * import { BudgetGuard, budgetGuardMiddleware } from \"floe-guard\";\n *\n * const guard = new BudgetGuard(5.00);\n * const model = wrapLanguageModel({\n * model: openai(\"gpt-4o\"),\n * middleware: budgetGuardMiddleware(guard),\n * });\n */\nexport function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware {\n return {\n async wrapGenerate({ doGenerate, model }) {\n const reserved = guard.reserve(); // throws BudgetExceeded before the call runs\n let result: any;\n try {\n result = await doGenerate!();\n } catch (err) {\n guard.release(reserved); // the call failed before settle() took ownership\n throw err;\n }\n // From here settle() OWNS the reservation: it releases the hold on its own\n // throw (unpriceable / non-finite cost) and consumes it on success. Releasing\n // again would double-subtract and clear a concurrent call's in-flight hold.\n let handled = false;\n try {\n const { promptTokens, completionTokens } = usageTokens(\n model.modelId,\n result?.usage,\n );\n handled = true;\n guard.settle(model.modelId, promptTokens, completionTokens, { reserved });\n return result;\n } catch (err) {\n if (!handled) guard.release(reserved); // failed reading usage, before settle()\n throw err;\n }\n },\n\n async wrapStream({ doStream, model }) {\n const reserved = guard.reserve(); // throws BudgetExceeded before the stream starts\n let streamResult: any;\n try {\n streamResult = await doStream!();\n } catch (err) {\n guard.release(reserved); // the call failed before settle() took ownership\n throw err;\n }\n const { stream, ...rest } = streamResult as {\n stream: ReadableStream<any>;\n } & Record<string, unknown>;\n\n // `handled` flips once the reservation is disposed — settled on the finish\n // part, or released exactly once if the stream produced no usage (flush) or\n // ended early via error/cancellation (cancel). settle() owns disposal once\n // called, so nothing else may release after `handled` is set, or we'd\n // double-subtract and clear a concurrent call's hold.\n let handled = false;\n const guarded = stream.pipeThrough(\n new TransformStream<any, any>({\n transform(chunk, controller) {\n if (chunk?.type === \"finish\" && !handled) {\n try {\n const { promptTokens, completionTokens } = usageTokens(\n model.modelId,\n chunk.usage,\n );\n handled = true;\n guard.settle(model.modelId, promptTokens, completionTokens, { reserved });\n } catch (err) {\n // Release only if we never reached settle() (e.g. usage missing).\n // If settle() itself threw, it already released its own hold.\n if (!handled) {\n handled = true;\n guard.release(reserved);\n }\n throw err;\n }\n }\n controller.enqueue(chunk);\n },\n flush() {\n // Clean close with no finish/usage part — free the held budget.\n if (!handled) {\n handled = true;\n guard.release(reserved);\n }\n },\n cancel() {\n // Upstream error or consumer cancellation: flush() does not run here\n // (Web Streams: flush and cancel are mutually exclusive), so release\n // the still-held reservation.\n if (!handled) {\n handled = true;\n guard.release(reserved);\n }\n },\n // `cancel` is valid per the Streams spec and supported in Node 18+, but\n // TS's Transformer lib type lags and omits it — cast to keep the type check.\n } as Transformer<any, any>),\n );\n\n return { stream: guarded, ...rest };\n },\n };\n}\n","import { BudgetExceeded } from \"./errors.js\";\nimport { type BudgetAdvisory, BudgetGuard } from \"./guard.js\";\n\nexport interface RetryPlan<T> {\n /** Operation to run for this retry attempt. */\n call: () => T | Promise<T>;\n /**\n * Estimated retry cost passed to `guard.check()` before the retry runs.\n * Leave undefined to use the guard's default last-call estimate.\n */\n estimatedCost?: number;\n}\n\nexport interface BudgetRetryOptions<T> {\n /** Estimated cost for retrying the original call. */\n estimatedCost?: number;\n /** Total attempts, including the first call. Default: 2. */\n maxAttempts?: number;\n /** Choose a cheaper retry plan when `guard.advisory().nearLimit` is true. */\n onDegrade?: (\n error: unknown,\n advisory: BudgetAdvisory,\n ) => RetryPlan<T> | Promise<RetryPlan<T> | undefined> | undefined;\n /** Decide whether an error is retryable. Defaults to all non-budget errors. */\n retryIf?: (error: unknown) => boolean;\n}\n\nfunction defaultRetryIf(error: unknown): boolean {\n return !(error instanceof BudgetExceeded);\n}\n\n/**\n * Run `call` with budget-aware retries.\n *\n * The first attempt runs unchanged. If it fails with a retryable error, the\n * helper retries as-is with ample budget, asks `onDegrade` for a cheaper plan\n * when near the limit, and always calls `guard.check(estimatedCost)` before a\n * retry so an over-budget retry is blocked before it runs.\n */\nexport async function withBudgetRetry<T>(\n guard: BudgetGuard,\n call: () => T | Promise<T>,\n options: BudgetRetryOptions<T> = {},\n): Promise<T> {\n const maxAttempts = options.maxAttempts ?? 2;\n if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {\n throw new RangeError(`maxAttempts must be an integer >= 1, got ${maxAttempts}`);\n }\n const retryIf = options.retryIf ?? defaultRetryIf;\n let plan: RetryPlan<T> = { call, estimatedCost: options.estimatedCost };\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n try {\n return await plan.call();\n } catch (error) {\n if (attempt >= maxAttempts || !retryIf(error)) {\n throw error;\n }\n plan = await nextPlan(guard, error, plan, options.onDegrade);\n }\n }\n\n throw new Error(\"unreachable\");\n}\n\nasync function nextPlan<T>(\n guard: BudgetGuard,\n error: unknown,\n current: RetryPlan<T>,\n onDegrade: BudgetRetryOptions<T>[\"onDegrade\"],\n): Promise<RetryPlan<T>> {\n const advisory = guard.advisory();\n if (advisory.nearLimit && onDegrade !== undefined) {\n const degraded = await onDegrade(error, advisory);\n if (degraded !== undefined) {\n guard.check(degraded.estimatedCost);\n return degraded;\n }\n }\n guard.check(current.estimatedCost);\n return current;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/sync.ts","../src/pricing.ts","../src/guard.ts","../src/latency.ts","../src/middleware.ts","../src/retry.ts"],"sourcesContent":["/**\n * Opt-in ledger sync — push the local spend ledger to Floe's Reconcile Mode.\n *\n * **Zero-telemetry is the default, and stays the default.** Nothing in this module\n * runs unless the caller *explicitly* opts in with a Floe API key — via\n * {@link BudgetGuard.enableSync} + {@link BudgetGuard.sync}. There is no implicit\n * enablement and no background send: the ledger leaves your process only when you\n * call one of those. Your ledger, your key, your choice.\n *\n * **Why sync at all.** Floe's gateway can't see spend it never routed — BYOK,\n * self-hosted, or off-path LLM/tool calls. Pushing your local ledger into Reconcile\n * Mode is how that spend lands on the ledger and your **Coverage Score** becomes\n * computable. Budget, not balance: this reports *what you already spent* for\n * coverage/attribution; it does not move money or change any wallet balance.\n *\n * **What leaves the process — exactly the {@link BudgetGuard.exportLog} JSONL** and\n * nothing else: one line per spend event, each a priced-cost record — `timestamp`,\n * `kind` (`\"llm\"`/`\"tool\"`), `model_or_tool`, `prompt_tokens`, `completion_tokens`,\n * `cost_usd`, and the optional `label` / `reserved` you set. **No prompts, no\n * message content, no identifiers** beyond a `label` you choose.\n *\n * Mirrors `src/floe_guard/sync.py` in the Python package — same behaviour\n * (https-only, key-required, fail-closed), transported over `fetch` instead of\n * `urllib`.\n *\n * Opt in: https://dev-dashboard.floelabs.xyz · https://floelabs.xyz\n */\n\nimport { LedgerSyncError } from \"./errors.js\";\n\nconst FLOE_API_KEY_ENV = \"FLOE_API_KEY\";\nconst FLOE_API_BASE_URL_ENV = \"FLOE_API_BASE_URL\";\nconst DEFAULT_BASE_URL = \"https://credit-api.floelabs.xyz\";\nconst LEDGER_SYNC_PATH = \"/v1/agents/ledger/sync\";\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n// The ONLY keys allowed to leave the process — the exportLog() schema. Enforced\n// (not just documented) in validateLedger(): a line carrying anything else is\n// rejected, so a hand-supplied file can't smuggle prompts / content / identifiers\n// past the privacy contract.\nconst ALLOWED_KEYS = new Set([\n \"timestamp\",\n \"kind\",\n \"model_or_tool\",\n \"prompt_tokens\",\n \"completion_tokens\",\n \"cost_usd\",\n \"label\",\n \"reserved\",\n]);\nconst REQUIRED_KEYS = [\"timestamp\", \"kind\", \"model_or_tool\", \"cost_usd\"];\n\n/**\n * Read an environment variable without a hard `process` reference — the package\n * compiles with `types: []` (no `@types/node`), so `process` is untyped. Reads\n * live at call time so tests that mutate the env are honoured.\n */\nfunction envVar(name: string): string {\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n return proc?.env?.[name] ?? \"\";\n}\n\n/**\n * Parse and validate every ledger line against the `exportLog()` schema.\n *\n * Throws {@link LedgerSyncError} on the first malformed / unknown-field / invalid\n * record, **before** anything is sent — so only priced spend events ever leave the\n * process, even when the ledger came from a hand-edited file. This is the privacy\n * contract *enforced*, not merely asserted: an **extra/unknown key** (a smuggled\n * `prompt`, `user_id`, …) is rejected outright. Mirrors `_validate_ledger` in the\n * Python client.\n *\n * (JS `typeof x === \"number\"` already excludes booleans and `Number.isInteger`\n * rejects them too, so — unlike Python, where `bool` is an `int` subclass — no\n * explicit boolean guards are needed.)\n */\nfunction validateLedger(jsonl: string): void {\n let i = 0;\n for (const raw of jsonl.split(\"\\n\")) {\n i += 1;\n const line = raw.trim();\n if (!line) continue;\n let event: unknown;\n try {\n event = JSON.parse(line);\n } catch (err) {\n throw new LedgerSyncError(`Ledger line ${i} is not valid JSON: ${describeCause(err)}`);\n }\n if (typeof event !== \"object\" || event === null || Array.isArray(event)) {\n throw new LedgerSyncError(`Ledger line ${i} is not a JSON object.`);\n }\n const ev = event as Record<string, unknown>;\n const extra = Object.keys(ev).filter((k) => !ALLOWED_KEYS.has(k));\n if (extra.length) {\n throw new LedgerSyncError(\n `Ledger line ${i} has fields outside the exportLog() schema: ` +\n `${JSON.stringify(extra.sort())}. Only priced spend events may be synced — ` +\n \"no prompts, content, or identifiers.\",\n );\n }\n const missing = REQUIRED_KEYS.filter((k) => !(k in ev));\n if (missing.length) {\n throw new LedgerSyncError(\n `Ledger line ${i} is missing required field(s): ${JSON.stringify(missing)}.`,\n );\n }\n if (ev.kind !== \"llm\" && ev.kind !== \"tool\") {\n throw new LedgerSyncError(\n `Ledger line ${i}: kind must be 'llm' or 'tool', got ${JSON.stringify(ev.kind)}.`,\n );\n }\n if (typeof ev.model_or_tool !== \"string\") {\n throw new LedgerSyncError(`Ledger line ${i}: model_or_tool must be a string.`);\n }\n const cost = ev.cost_usd;\n if (typeof cost !== \"number\" || !Number.isFinite(cost) || cost < 0) {\n throw new LedgerSyncError(\n `Ledger line ${i}: cost_usd must be a finite, non-negative number, got ${JSON.stringify(cost)}.`,\n );\n }\n const ts = ev.timestamp;\n if (typeof ts !== \"number\" || !Number.isFinite(ts)) {\n throw new LedgerSyncError(\n `Ledger line ${i}: timestamp must be a finite number, got ${JSON.stringify(ts)}.`,\n );\n }\n for (const field of [\"prompt_tokens\", \"completion_tokens\"] as const) {\n const value = ev[field];\n // Absent (undefined) or explicit null are fine — tool events carry null; a\n // present value must be an integer.\n if (value !== undefined && value !== null && !Number.isInteger(value)) {\n throw new LedgerSyncError(`Ledger line ${i}: ${field} must be an integer or null.`);\n }\n }\n if (\"label\" in ev && typeof ev.label !== \"string\") {\n throw new LedgerSyncError(`Ledger line ${i}: label must be a string.`);\n }\n if (\"reserved\" in ev) {\n const reserved = ev.reserved;\n if (typeof reserved !== \"number\" || !Number.isFinite(reserved)) {\n throw new LedgerSyncError(`Ledger line ${i}: reserved must be a finite number.`);\n }\n }\n }\n}\n\n/**\n * POST an {@link BudgetGuard.exportLog} JSONL ledger to Reconcile Mode and resolve\n * with the number of events the server accepted.\n *\n * This is the ONLY function that sends the ledger, and it sends only when called.\n * An empty ledger is a no-op (resolves `0` with **no** `fetch` call).\n *\n * @param jsonl the ledger as newline-delimited JSON — `exportLog()` output.\n * @param apiKey Floe agent key (`floe_<hex>`, `read_write`). Defaults to the\n * `FLOE_API_KEY` env var.\n * @param opts.baseUrl API base. Defaults to `FLOE_API_BASE_URL`, else the prod host.\n * @param opts.timeoutMs request timeout in milliseconds (default 30_000).\n * @throws {LedgerSyncError} missing key, a non-https/malformed base URL, a non-2xx\n * response, network/timeout, or a malformed response body. The key and ledger are\n * never sent over a non-https or malformed URL.\n */\nexport async function pushLedger(\n jsonl: string,\n apiKey?: string,\n opts: { baseUrl?: string; timeoutMs?: number } = {},\n): Promise<number> {\n // An empty ledger never touches the network — nothing to send.\n if (!jsonl.trim()) return 0;\n\n // Enforce the privacy contract BEFORE any network: only exportLog() events —\n // no prompts, content, or identifiers — may leave, even from a hand-supplied file.\n validateLedger(jsonl);\n\n const key = ((apiKey ?? \"\") || envVar(FLOE_API_KEY_ENV)).trim();\n if (!key) {\n throw new LedgerSyncError(\n `No Floe API key: pass apiKey or set ${FLOE_API_KEY_ENV}. ` +\n \"Sync is opt-in and needs your key.\",\n );\n }\n\n const envBase = envVar(FLOE_API_BASE_URL_ENV).trim();\n const base = ((opts.baseUrl ?? \"\").trim() || envBase || DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n let parsed: URL;\n try {\n parsed = new URL(base);\n } catch {\n // The request carries the Floe agent key as a bearer token AND your spend\n // ledger — never send either over a malformed URL.\n throw new LedgerSyncError(\n `Refusing to send the ledger to '${base}': ` +\n \"the base URL must be an https:// URL with a host.\",\n );\n }\n if (parsed.protocol !== \"https:\" || !parsed.host) {\n // Same guard for a well-formed but non-https (or hostless) URL.\n throw new LedgerSyncError(\n `Refusing to send the ledger to '${base}': ` +\n \"the base URL must be an https:// URL with a host.\",\n );\n }\n const url = `${base}${LEDGER_SYNC_PATH}`;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n // Refuse redirects: a 3xx would otherwise let the key + ledger be re-sent to\n // another host. (The Fetch spec strips `Authorization` on a cross-origin\n // redirect, but `redirect: \"error\"` is the belt-and-suspenders — nothing is\n // re-sent anywhere, same-origin or not.)\n redirect: \"error\",\n headers: {\n Authorization: `Bearer ${key}`,\n \"Content-Type\": \"application/x-ndjson\",\n Accept: \"application/json\",\n },\n body: jsonl,\n signal: controller.signal,\n });\n } catch (err) {\n throw new LedgerSyncError(`Could not reach Floe at ${url}: ${describeCause(err)}`);\n } finally {\n clearTimeout(timer);\n }\n\n if (!response.ok) {\n throw new LedgerSyncError(await describeHttpError(response));\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(await response.text());\n } catch (err) {\n throw new LedgerSyncError(`Malformed JSON from Floe at ${url}: ${describeCause(err)}`);\n }\n if (typeof payload !== \"object\" || payload === null || Array.isArray(payload)) {\n throw new LedgerSyncError(`Unexpected response shape from Floe at ${url}.`);\n }\n // \"synced\" is the count the server accepted (new rows); \"duplicates\" (already\n // ingested, idempotent) are not counted here. Absent → 0; present must be a real\n // non-negative integer — reject bool/float/negative/garbage rather than coerce.\n const synced = (payload as Record<string, unknown>).synced;\n if (synced === undefined || synced === null) return 0;\n if (typeof synced !== \"number\" || !Number.isInteger(synced) || synced < 0) {\n throw new LedgerSyncError(`Invalid 'synced' count from Floe at ${url}: ${JSON.stringify(synced)}.`);\n }\n return synced;\n}\n\nfunction describeCause(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nasync function describeHttpError(response: Response): Promise<string> {\n let detail = \"\";\n try {\n const data: unknown = JSON.parse(await response.text());\n if (\n typeof data === \"object\" &&\n data !== null &&\n typeof (data as Record<string, unknown>).error === \"string\"\n ) {\n detail = ` (${(data as Record<string, unknown>).error as string})`;\n }\n } catch {\n // Non-JSON / empty error body — the status code alone is the message.\n }\n const code = response.status;\n if (code === 401) return `Floe rejected the API key (401 unauthorized)${detail}.`;\n if (code === 403) {\n return (\n `Floe refused the sync (403 forbidden)${detail} — the agent may be ` +\n \"closed/suspended, or the key is read-only (a read_write key is required).\"\n );\n }\n return `Floe returned HTTP ${code}${detail}.`;\n}\n","/**\n * Offline token pricing from a vendored LiteLLM cost map.\n *\n * Mirrors `src/floe_guard/pricing.py`: BOTH the input and output per-token prices\n * must be finite numbers, otherwise the model is treated as unpriceable. A\n * half-valid entry would silently undercharge — so we refuse it (fail-closed).\n *\n * No network. The cost map (`cost_map.json`) is a snapshot of LiteLLM's\n * `model_prices_and_context_window.json`; refresh it on a schedule or estimates\n * drift as vendors change prices.\n */\n\nimport costMapJson from \"./cost_map.json\";\n\n/** A user-supplied per-token price, in USD, for a model the map cannot price. */\nexport interface ManualPrice {\n inputCostPerToken: number;\n outputCostPerToken: number;\n}\n\n/** A resolved per-token price plus where it came from. */\nexport interface PricedModel {\n inputCostPerToken: number;\n outputCostPerToken: number;\n source: \"override\" | \"cost_map\";\n}\n\ninterface CostMapEntry {\n input_cost_per_token?: unknown;\n output_cost_per_token?: unknown;\n}\n\nconst COST_MAP = costMapJson as Record<string, CostMapEntry>;\n\n/**\n * The one `<provider>/` prefix that is safe to strip: the remainder of a\n * `groq/…` id is the ChatGroq id the map vendors (e.g. `groq/qwen/qwen3-32b` →\n * `qwen/qwen3-32b`). `openai/` and `anthropic/` are deliberately excluded:\n * their own model ids never contain slashes (single-segment remainders are\n * already covered by the bare-last-segment fallback), so a multi-segment\n * remainder under those prefixes is some OTHER vendor's model behind an\n * OpenAI-compatible endpoint (vLLM, OpenRouter, …) — bridging it into a\n * Groq-priced key would under-meter. Unknown prefixes fail closed the same way.\n */\nconst PROVIDER_PREFIXES = new Set([\"groq\"]);\n\n/**\n * A trailing dated-snapshot suffix: Anthropic's `-20250929` or OpenAI's\n * `-2024-08-06`. Vendors resolve alias ids to dated snapshots in responses, so a\n * snapshot the map doesn't list yet prices at its alias entry (same model, same\n * rate) instead of failing closed. (`\\d` is ASCII-only in JS; the Python regex\n * uses re.ASCII to match.)\n */\nconst DATE_SUFFIX = /-(?:\\d{8}|\\d{4}-\\d{2}-\\d{2})$/;\n\n/**\n * Lookup keys for a model id in two specificity groups, deduplicated.\n *\n * Group 1 (exact): the raw id, the id with a known `provider/` first segment\n * stripped, the bare last segment. Group 2 (date-stripped): the same forms\n * with a trailing dated-snapshot suffix removed. Kept separate so a\n * less-specific date-stripped key (in overrides OR the map) can never shadow\n * an exact dated entry — e.g. an alias override must not absorb a snapshot\n * the map prices differently.\n */\nfunction candidateGroups(model: string): [string[], string[]] {\n const m = model.trim();\n const base = [m];\n const firstSlash = m.indexOf(\"/\");\n if (firstSlash !== -1 && PROVIDER_PREFIXES.has(m.slice(0, firstSlash))) {\n base.push(m.slice(firstSlash + 1));\n }\n const lastSlash = m.lastIndexOf(\"/\");\n if (lastSlash !== -1) {\n base.push(m.slice(lastSlash + 1));\n }\n const exact: string[] = [];\n for (const cand of base) {\n if (cand && !exact.includes(cand)) exact.push(cand);\n }\n const stripped: string[] = [];\n for (const cand of exact) {\n const c = cand.replace(DATE_SUFFIX, \"\");\n if (c && !exact.includes(c) && !stripped.includes(c)) stripped.push(c);\n }\n return [exact, stripped];\n}\n\nfunction bothFinite(a: unknown, b: unknown): boolean {\n return (\n typeof a === \"number\" &&\n typeof b === \"number\" &&\n Number.isFinite(a) &&\n Number.isFinite(b)\n );\n}\n\n/**\n * Resolve a model to its per-token price, or `null` if it cannot be priced.\n *\n * Per specificity group (exact forms first, date-stripped fallbacks second):\n * overrides win, then the bundled cost map. Fail-closed: the first matching\n * entry must have finite prices, else `null`.\n */\nexport function resolvePrice(\n model: string,\n overrides?: Record<string, ManualPrice>,\n): PricedModel | null {\n for (const cands of candidateGroups(model)) {\n if (overrides) {\n for (const cand of cands) {\n // Own-property check so a key like \"constructor\" can't pull a function\n // off Object.prototype and be treated as a price entry.\n const ov = Object.prototype.hasOwnProperty.call(overrides, cand)\n ? overrides[cand]\n : undefined;\n if (ov !== undefined) {\n if (bothFinite(ov.inputCostPerToken, ov.outputCostPerToken)) {\n return {\n inputCostPerToken: ov.inputCostPerToken,\n outputCostPerToken: ov.outputCostPerToken,\n source: \"override\",\n };\n }\n return null;\n }\n }\n }\n\n for (const cand of cands) {\n const entry = Object.prototype.hasOwnProperty.call(COST_MAP, cand)\n ? COST_MAP[cand]\n : undefined;\n if (!entry) continue;\n\n const input = entry.input_cost_per_token;\n const output = entry.output_cost_per_token;\n if (!bothFinite(input, output)) return null;\n\n return {\n inputCostPerToken: input as number,\n outputCostPerToken: output as number,\n source: \"cost_map\",\n };\n }\n }\n return null;\n}\n\n/** USD cost for token usage. Negative counts are clamped to zero. */\nexport function priceTokens(\n priced: PricedModel,\n promptTokens: number,\n completionTokens: number,\n): number {\n const p = Math.max(0, promptTokens);\n const c = Math.max(0, completionTokens);\n const cost = p * priced.inputCostPerToken + c * priced.outputCostPerToken;\n if (!Number.isFinite(cost)) {\n // Defense-in-depth: resolvePrice already guarantees finite rates.\n throw new Error(\"Non-finite LLM cost — pricing entry is invalid\");\n }\n return Math.max(0, cost);\n}\n","/**\n * The local, in-process budget guard.\n *\n * `BudgetGuard` is a kill-switch that lives in the LLM call path. The contract:\n *\n * 1. Call {@link BudgetGuard.check} BEFORE every LLM call. If the *next* call\n * would cross the ceiling, it throws {@link BudgetExceeded} and the call never\n * runs.\n * 2. Call {@link BudgetGuard.record} AFTER every response, with the token usage.\n * It prices the tokens offline and accrues the USD into a running total.\n *\n * **Concurrency.** `check()` then `record()` is a check-then-act with an `await`\n * in between. Fire several model calls at once (e.g. `Promise.all`) and they all\n * `check()` against the same under-limit total before any `record()` lands, so\n * the ceiling is blown (see issue #18). {@link BudgetGuard.reserve} /\n * {@link BudgetGuard.settle} close that gap: `reserve()` holds the estimated cost\n * in flight (synchronously, before the await), so parallel callers each take\n * their own slice of the ceiling. JS is single-threaded, so an in-flight counter\n * is enough — no lock needed. The middleware uses it; `check`/`record` are\n * unchanged.\n *\n * This is a faithful port of `src/floe_guard/guard.py` — same prediction logic,\n * same epsilon handling, same fail-closed default.\n */\n\nimport { BudgetExceeded, TokenBudgetExceeded, UnpriceableModelError } from \"./errors.js\";\nimport { pushLedger } from \"./sync.js\";\nimport {\n type ManualPrice,\n priceTokens,\n resolvePrice,\n} from \"./pricing.js\";\n\n/** Tolerance for float rounding in the running spend total (well below $0.000001). */\nconst EPS = 1e-12;\n\n/**\n * A two-dimensional in-flight hold (USD + tokens) returned by\n * {@link BudgetGuard.reserve} when a token ceiling or an active\n * {@link BudgetGuard.step} is involved. A dumb value handle — pass it straight\n * back to {@link BudgetGuard.settle} / {@link BudgetGuard.release}. When neither\n * tokens nor a step are involved, `reserve` returns a plain `number` instead\n * (byte-for-byte the old behaviour), so {@link ReservationHandle} is the union.\n */\nexport interface BudgetReservation {\n readonly usd: number;\n readonly tokens: number;\n}\n\n/**\n * A plain `number` when USD-only (backward compatible); a\n * {@link BudgetReservation} when a token ceiling or an active step is in play.\n */\nexport type ReservationHandle = number | BudgetReservation;\n\nfunction isReservation(h: ReservationHandle): h is BudgetReservation {\n return (\n typeof h === \"object\" &&\n h !== null &&\n typeof (h as BudgetReservation).usd === \"number\" &&\n typeof (h as BudgetReservation).tokens === \"number\"\n );\n}\n\n/** One scope on the step stack (see {@link BudgetGuard.step}). Mutable. */\ninterface StepState {\n readonly maxUsd: number | null;\n readonly maxTokens: number | null;\n spentUsd: number;\n spentTokens: number;\n reservedUsd: number;\n reservedTokens: number;\n}\n\n/**\n * One priced spend event in the guard's per-call ledger.\n *\n * Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /\n * {@link BudgetGuard.recordTool} / {@link BudgetGuard.settleTool} that accrues\n * spend appends exactly one event, so\n * the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has\n * evicted old events). The schema is identical in the Python\n * package (`SpendEvent` in `src/floe_guard/guard.py`) and\n * {@link BudgetGuard.exportLog} serialises it with the same snake_case keys in\n * both languages, so every agent emits the same shape regardless of stack.\n */\nexport interface SpendEvent {\n /** Unix epoch seconds (UTC). */\n readonly timestamp: number;\n readonly kind: \"llm\" | \"tool\";\n readonly modelOrTool: string;\n /** `null` for tool events. */\n readonly promptTokens: number | null;\n /** `null` for tool events. */\n readonly completionTokens: number | null;\n readonly costUsd: number;\n /** Caller-supplied tag (agent/task name). */\n readonly label?: string;\n /** The reservation settled by this call, if any. */\n readonly reserved?: number;\n}\n\nexport interface BudgetGuardOptions {\n /** Per-model manual prices for models the bundled cost map cannot price. */\n priceOverrides?: Record<string, ManualPrice>;\n /**\n * When `true` (default), recording an unpriceable model without a manual price\n * warns loudly AND throws {@link UnpriceableModelError}. When `false`, it warns\n * and skips accrual (you have opted into un-enforced spend for that model).\n */\n failClosed?: boolean;\n /**\n * Optional callback invoked with `(spentUsd, limitUsd)` right before\n * {@link BudgetExceeded} is thrown. Defaults to printing the\n * `BUDGET EXCEEDED — call blocked` banner to stderr. Fires on **USD** ceiling\n * crossings only; a token-ceiling block ({@link TokenBudgetExceeded}, aggregate\n * or step) is thrown without invoking it, since the callback is dollar-shaped.\n */\n onBlock?: (spentUsd: number, limitUsd: number) => void;\n /**\n * Utilization (basis points, 0..10000) at which {@link BudgetGuard.advisory}\n * flags `nearLimit` so an agent can taper before the hard-stop. Default 8000.\n */\n nearLimitBps?: number;\n /**\n * Optional cap on the per-call spend ledger ({@link BudgetGuard.spendLog}).\n * When set, the ledger is a ring buffer keeping the most recent N events so a\n * long-running agent's memory stays bounded; the running totals are\n * unaffected. Default: keep every event.\n */\n maxLogEvents?: number;\n /**\n * Aggregate token ceiling. `null`/undefined disables the token dimension\n * entirely (a pure USD guard, unchanged). `0` blocks the first token.\n */\n tokenLimit?: number;\n}\n\n/**\n * A context-aware spend signal for the single local budget.\n *\n * Mirrors the core fields of hosted Floe's `X-Floe-Budget-Advisory` header, so\n * agent logic that reads it (taper as you approach the cap, stop at it) ports\n * unchanged to the hosted path. Hosted adds what a local, single-budget guard\n * cannot know: which of several caps is tightest (`scope` across\n * `credit_line | session | task | api | vendor`), cross-vendor reasoning,\n * server-truth balances, and rolling-window reset timing.\n *\n * This is a **soft** signal — the model may ignore it. The hard-stop\n * ({@link BudgetGuard.check}) is what enforces the ceiling; the advisory is\n * upside (let the agent finish on budget rather than be cut off).\n */\nexport interface BudgetAdvisory {\n nearLimit: boolean;\n /** Utilization in basis points, 0..10000 (8500 = 85%). */\n usedBps: number;\n remainingUsd: number;\n limitUsd: number;\n spentUsd: number;\n /** Hosted reports the tightest cap across all scopes; local is always \"local\". */\n scope: \"local\";\n /**\n * The guard's own next-call estimate (the costlier of the last LLM and last\n * tool call — the same value the default reservation uses). 0 until the first\n * call is recorded, so a planner can't divide by a cold estimate.\n *\n * Optional so adding it stays a non-breaking, additive change for any code\n * that constructs a `BudgetAdvisory` literal; `advisory()` always sets it.\n */\n expectedCost?: number;\n /**\n * How many more calls the remaining budget buys at expectedCost:\n * floor(remainingUsd / expectedCost). null when expectedCost is 0 (no call\n * recorded yet) — unknown, not zero. Optional for the same additive reason as\n * expectedCost; `advisory()` always sets it.\n */\n estCallsRemaining?: number | null;\n /**\n * Aggregate token utilization in basis points (0..10000), mirroring usedBps\n * for the token ceiling. null when no `tokenLimit` is set (nothing to signal).\n */\n tokenUsedBps?: number | null;\n /** Tokens left before the aggregate token ceiling. null when no `tokenLimit`. */\n remainingTokens?: number | null;\n /**\n * Per-step headroom for the innermost active step — present (non-null) ONLY\n * while a step() is active AND that dimension is capped. null otherwise.\n */\n stepRemainingUsd?: number | null;\n stepRemainingTokens?: number | null;\n /**\n * Spend rate in USD per minute over this guard's lifetime (spentUsd ÷ minutes\n * since the guard was created) — the number voice teams watch. null only when no\n * wall-clock time has elapsed yet. Make one guard per call/turn for a per-call\n * burn rate; a long-lived guard reports the session average. Optional for the\n * same additive reason as the fields above; `advisory()` always sets it.\n */\n burnRateUsdPerMin?: number | null;\n}\n\nexport class BudgetGuard {\n readonly limitUsd: number;\n spentUsd = 0;\n /**\n * Wall-clock start (ms since epoch) of this guard's spend window, for the $/min\n * burn rate (advisory().burnRateUsdPerMin). One guard per call/turn ⇒ a per-call\n * rate. Set at construction; tests set it directly to control elapsed time.\n */\n createdAtMs = Date.now();\n priceOverrides?: Record<string, ManualPrice>;\n failClosed: boolean;\n nearLimitBps: number;\n /** Aggregate token ceiling, or null when the token dimension is disabled. */\n readonly tokenLimit: number | null;\n /** Aggregate tokens accrued (prompt + completion + cache buckets). */\n spentTokens = 0;\n /** Tokens held in-flight — the token twin of `reserved`. */\n private reservedTokens = 0;\n /** Step stack: innermost step is last. Empty when no step() is active. */\n private readonly steps: StepState[] = [];\n\n private readonly onBlock: (spentUsd: number, limitUsd: number) => void;\n /**\n * Costs of the most recent priced LLM call and tool call, tracked\n * SEPARATELY: the default next-call prediction is the max of the two, so a\n * cheap tool call can't shrink the estimate right before an expensive LLM\n * call (or vice versa) — conservative beats one-call-too-late.\n */\n private lastLlmCost = 0;\n private lastToolCost = 0;\n /** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */\n private reserved = 0;\n /** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */\n private readonly spendEvents: SpendEvent[] = [];\n private readonly maxLogEvents?: number;\n /**\n * Per-tool running totals (settleTool/recordTool) — the tool side of the one\n * shared ceiling, exposed via the toolCosts getter. null-prototype: tool\n * names are caller-supplied strings, so a \"__proto__\" name is stored as\n * plain data instead of mutating the object's prototype.\n */\n private readonly toolCostTotals: Record<string, number> = Object.create(null);\n\n /**\n * Opt-in ledger sync — OFF by default (zero-telemetry). No key is stored and\n * nothing is ever sent until enableSync() is called; see enableSync()/sync().\n */\n private syncEnabled = false;\n private syncKey: string | null = null;\n private syncBase: string | null = null;\n\n /**\n * @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.\n */\n constructor(limitUsd: number, options: BudgetGuardOptions = {}) {\n if (!Number.isFinite(limitUsd) || limitUsd < 0) {\n // NaN/Infinity would make every check() comparison fail-open, silently\n // disabling the guard — reject them up front.\n throw new RangeError(\n `limitUsd must be a finite, non-negative number, got ${limitUsd}`,\n );\n }\n // `=== undefined` (not `??`) so an explicit null is rejected by validation\n // rather than silently defaulting — matches Python, which rejects None.\n const nearLimitBps = options.nearLimitBps === undefined ? 8000 : options.nearLimitBps;\n if (!Number.isInteger(nearLimitBps) || nearLimitBps < 0 || nearLimitBps > 10000) {\n throw new RangeError(\n `nearLimitBps must be an integer in 0..10000, got ${nearLimitBps}`,\n );\n }\n if (\n options.maxLogEvents !== undefined &&\n (!Number.isInteger(options.maxLogEvents) || options.maxLogEvents < 0)\n ) {\n throw new RangeError(\n `maxLogEvents must be a non-negative integer, got ${options.maxLogEvents}`,\n );\n }\n if (\n options.tokenLimit !== undefined &&\n (!Number.isInteger(options.tokenLimit) || options.tokenLimit < 0)\n ) {\n throw new RangeError(\n `tokenLimit must be a non-negative integer, got ${options.tokenLimit}`,\n );\n }\n this.limitUsd = limitUsd;\n this.tokenLimit = options.tokenLimit ?? null;\n this.maxLogEvents = options.maxLogEvents;\n this.priceOverrides = options.priceOverrides;\n this.failClosed = options.failClosed ?? true;\n this.onBlock = options.onBlock ?? defaultOnBlock;\n this.nearLimitBps = nearLimitBps;\n }\n\n /**\n * Throw {@link BudgetExceeded} if the next call would cross the ceiling.\n *\n * Call this immediately before each LLM request. The \"next call\" is estimated\n * conservatively as the costlier of the last LLM call and the last tool call\n * (override with `estimatedNextCost`); the\n * first call is always allowed unless the ceiling is already met. In-flight\n * reservations count toward the total, so this stays correct alongside\n * {@link BudgetGuard.reserve}.\n *\n * Note: `check` is a non-binding peek. For parallel calls, use `reserve()` /\n * `settle()`, which hold the estimate across the await.\n */\n check(estimatedNextCost?: number, options: { estimatedTokens?: number } = {}): void {\n const rawEstimate =\n estimatedNextCost === undefined ? this.defaultEstimate() : estimatedNextCost;\n if (!Number.isFinite(rawEstimate)) {\n // NaN/Infinity would poison the comparisons and fail-open — reject it\n // (parity with the constructor's Number.isFinite guard).\n throw new RangeError(\n `estimatedNextCost must be a finite number, got ${rawEstimate}`,\n );\n }\n const estimate = Math.max(0, rawEstimate);\n this.validateTokenEstimate(options.estimatedTokens);\n const tokens = Math.max(0, options.estimatedTokens ?? 0);\n const blocked = this.blockingCross(estimate, tokens);\n if (blocked !== null) this.raiseBlock(blocked);\n }\n\n /**\n * Atomically check the ceiling AND hold the estimated cost in flight.\n *\n * The concurrency-safe enforcement path: call before the request and hold the\n * returned reservation across the await, so parallel callers can't all clear\n * the same stale total. Throws {@link BudgetExceeded} (without reserving) if\n * the reservation would cross the ceiling. Returns the reservation handle to\n * pass to {@link BudgetGuard.settle} (or {@link BudgetGuard.release} on error).\n * `estimatedCost` defaults to the costlier of the last LLM call and the last\n * tool call.\n */\n reserve(estimatedCost?: number, options: { estimatedTokens?: number } = {}): ReservationHandle {\n const rawEstimate = estimatedCost === undefined ? this.defaultEstimate() : estimatedCost;\n if (!Number.isFinite(rawEstimate)) {\n // NaN would poison this.reserved and fail-open the ceiling — reject it.\n throw new RangeError(\n `estimatedCost must be a finite number, got ${rawEstimate}`,\n );\n }\n const estimate = Math.max(0, rawEstimate);\n // Reject a fractional/NaN/Infinity token estimate BEFORE mutating\n // reservedTokens/step holds — a NaN would fail-open the integer token\n // comparisons and corrupt the tally. A negative int is clamped by Math.max.\n this.validateTokenEstimate(options.estimatedTokens);\n const tokens = Math.max(0, options.estimatedTokens ?? 0);\n const blocked = this.blockingCross(estimate, tokens);\n if (blocked !== null) this.raiseBlock(blocked);\n this.reserved += estimate;\n this.reservedTokens += tokens;\n const step = this.steps.length ? this.steps[this.steps.length - 1] : null;\n if (step !== null) {\n step.reservedUsd += estimate;\n step.reservedTokens += tokens;\n }\n // Plain number only when nothing token- or step-shaped is in play, so\n // pre-existing USD-only callers get byte-for-byte the old handle.\n if (tokens === 0 && step === null) return estimate;\n return { usd: estimate, tokens };\n }\n\n /**\n * Release a reservation and record the actual cost. `record` is `settle` with\n * no reservation. Returns the USD cost of this call; unpriceable-model handling\n * matches {@link BudgetGuard.record}, and any held reservation is released even\n * on the warn-and-skip path. A priced call appends one {@link SpendEvent} to\n * {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);\n * the warn-and-skip path accrues nothing and logs nothing, so the ledger stays\n * in lockstep with `spentUsd`.\n */\n settle(\n model: string,\n promptTokens: number,\n completionTokens: number,\n options: { reserved?: ReservationHandle; price?: ManualPrice; label?: string } = {},\n ): number {\n const reserved = options.reserved ?? 0;\n // A bad reserved handle would corrupt this.reserved and break the ceiling for\n // OTHER in-flight calls (negative → phantom hold; Infinity → clears all holds).\n // reservedUsdOf validates the handle (raw number, or a BudgetReservation's fields).\n const reservedUsd = this.reservedUsdOf(reserved);\n let overrides = this.priceOverrides;\n if (options.price !== undefined) {\n overrides = { ...(overrides ?? {}), [model]: options.price };\n }\n\n const priced = resolvePrice(model, overrides);\n if (priced === null) {\n console.warn(\n `Cannot price model '${model}': not in the bundled cost map and no ` +\n `manual price given. The budget guard cannot enforce a ceiling on ` +\n `spend it cannot measure — pass { price } or set it in priceOverrides.`,\n );\n // Release any held reservation on BOTH paths. Fail-closed must not leak\n // the in-flight hold, or reserved grows permanently and remainingUsd\n // shrinks until reserve() starts blocking everything.\n this.release(reserved);\n if (this.failClosed) {\n throw new UnpriceableModelError(model);\n }\n return 0;\n }\n\n let cost: number;\n try {\n cost = priceTokens(priced, promptTokens, completionTokens);\n } catch (err) {\n // priceTokens can throw (e.g. non-finite costs). Release the in-flight\n // hold before re-throwing so `reserved` doesn't leak and shrink\n // remainingUsd permanently — same fail-safe as the unpriceable path above.\n this.release(reserved);\n throw err;\n }\n if (reserved) {\n this.consumeReservation(reserved);\n }\n // Tokens accrued match what priceTokens bills (prompt + completion; negative\n // counts clamp to 0, as in pricing).\n const accruedTokens = Math.max(0, promptTokens) + Math.max(0, completionTokens);\n this.spentUsd += cost;\n this.spentTokens += accruedTokens;\n this.accrueStep(cost, accruedTokens);\n // Clamp a sub-epsilon float overshoot back to the limit so the running total\n // never reports as having crossed the ceiling by a rounding artifact.\n if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {\n this.spentUsd = this.limitUsd;\n }\n this.lastLlmCost = cost;\n this.appendEvent({\n timestamp: Date.now() / 1000,\n kind: \"llm\",\n modelOrTool: model,\n promptTokens,\n completionTokens,\n costUsd: cost,\n ...(options.label !== undefined ? { label: options.label } : {}),\n // 0 means \"no reservation\" (the plain record() path) — omit rather than\n // log a meaningless zero. Log the USD amount so the ledger schema stays\n // number-shaped even for a BudgetReservation.\n ...(reservedUsd ? { reserved: reservedUsd } : {}),\n });\n return cost;\n }\n\n /**\n * Price one response's tokens offline and add the cost to the total.\n *\n * Returns the USD cost of this call. If the model is unpriceable and no `price`\n * is given, behaviour depends on `failClosed`: warn + throw (default), or\n * warn + skip accrual.\n */\n record(\n model: string,\n promptTokens: number,\n completionTokens: number,\n options: { price?: ManualPrice; label?: string } = {},\n ): number {\n return this.settle(model, promptTokens, completionTokens, {\n reserved: 0,\n price: options.price,\n label: options.label,\n });\n }\n\n /**\n * Atomically check the ceiling AND hold a tool call's cost in flight.\n *\n * The tool-spend counterpart of {@link BudgetGuard.reserve} — and STRONGER\n * than the LLM path, because a paid tool's price is usually known exactly\n * before the call, so the pre-call hard-stop is precise rather than\n * estimated:\n *\n * const handle = guard.reserveTool(0.02); // throws BEFORE Apollo runs\n * const result = await apollo.peopleLookup(...);\n * guard.settleTool(\"apollo.people_lookup\", 0.02, { reserved: handle });\n *\n * Throws {@link BudgetExceeded} (without reserving) if the call would cross\n * the ceiling. The estimate is required — tools have no last-cost prediction\n * worth falling back to. Pass the returned handle to\n * {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.\n */\n reserveTool(estimatedCost: number): ReservationHandle {\n if (estimatedCost === undefined) {\n // reserve(undefined) would silently fall back to the last-cost prediction\n // (0 on a fresh guard) — an unguarded tool call. A missing price must\n // fail loudly, e.g. guard.reserveTool(priceTable[tool]).\n throw new RangeError(\"reserveTool requires an estimated cost, got undefined\");\n }\n if (!Number.isFinite(estimatedCost) || estimatedCost < 0) {\n // reserve() clamps a negative estimate to 0 (lenient LLM contract) — for\n // a tool that would reserve nothing: the same unguarded call.\n throw new RangeError(\n `estimatedCost must be a finite, non-negative number, got ${estimatedCost}`,\n );\n }\n return this.reserve(estimatedCost);\n }\n\n /**\n * Release a reservation and record a tool call's actual cost.\n *\n * `recordTool` is `settleTool` with no reservation. The caller supplies the\n * cost — tools have no token usage to price. Accrues into the same\n * `spentUsd` ceiling as tokens, tallies the per-tool total\n * ({@link BudgetGuard.toolCosts}), updates the tool side of the next-call\n * estimate (tracked separately from the LLM side; the default prediction is\n * the max of the two, so a tool-hammering loop's plain `check()` stops\n * BEFORE the crossing call without a cheap tool shrinking the LLM\n * prediction), and appends\n * a `kind: \"tool\"` {@link SpendEvent} to {@link BudgetGuard.spendLog}.\n * Returns `costUsd`.\n */\n settleTool(\n tool: string,\n costUsd: number,\n options: { reserved?: ReservationHandle; label?: string } = {},\n ): number {\n if (!Number.isFinite(costUsd) || costUsd < 0) {\n throw new RangeError(`costUsd must be a finite, non-negative number, got ${costUsd}`);\n }\n const reserved = options.reserved ?? 0;\n // A bad reserved handle would corrupt the in-flight tally and break the\n // ceiling for OTHER calls — same contract as settle(). reservedUsdOf\n // validates the handle (raw number, or a BudgetReservation's fields).\n const reservedUsd = this.reservedUsdOf(reserved);\n if (reserved) {\n this.consumeReservation(reserved);\n }\n this.spentUsd += costUsd;\n // A paid tool spends the step's USD too (no tokens to price).\n this.accrueStep(costUsd, 0);\n // Same sub-epsilon clamp as settle(): never report a rounding-artifact\n // crossing of the ceiling.\n if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {\n this.spentUsd = this.limitUsd;\n }\n this.lastToolCost = costUsd;\n this.toolCostTotals[tool] = (this.toolCostTotals[tool] ?? 0) + costUsd;\n this.appendEvent({\n timestamp: Date.now() / 1000,\n kind: \"tool\",\n modelOrTool: tool,\n promptTokens: null,\n completionTokens: null,\n costUsd,\n ...(options.label !== undefined ? { label: options.label } : {}),\n ...(reservedUsd ? { reserved: reservedUsd } : {}),\n });\n return costUsd;\n }\n\n /**\n * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.\n *\n * Post-hoc accrual for costs only known after the call (metered APIs); when\n * the price is known up front, {@link BudgetGuard.reserveTool} /\n * {@link BudgetGuard.settleTool} give the stronger pre-call hard-stop. See\n * `settleTool` for the full contract. Returns `costUsd`.\n */\n recordTool(tool: string, costUsd: number, options: { label?: string } = {}): number {\n return this.settleTool(tool, costUsd, { reserved: 0, label: options.label });\n }\n\n /**\n * Drop an in-flight reservation without recording spend (e.g. the call failed\n * before producing usage). Safe to call with `0`.\n */\n release(reserved: ReservationHandle): void {\n // Validate before the zero-check so a NaN handle throws instead of being\n // silently dropped (a leak); a bad handle corrupts the in-flight tally.\n // reservedUsdOf validates the handle (raw number, or a BudgetReservation's fields).\n this.reservedUsdOf(reserved);\n if (!reserved) return;\n this.consumeReservation(reserved);\n }\n\n /** USD left before the ceiling, net of in-flight reservations (never negative). */\n get remainingUsd(): number {\n return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);\n }\n\n /**\n * Per-tool running USD totals, keyed by the name given to `settleTool()` /\n * `recordTool()` — e.g. `{\"apollo.people_lookup\": 0.42, \"exa.search\": 0.11}`.\n * Makes the token/tool split of the one shared ceiling inspectable\n * (`spentUsd - sum of toolCosts` is the token side). Returns a snapshot copy.\n */\n get toolCosts(): Record<string, number> {\n return { ...this.toolCostTotals };\n }\n\n /**\n * The per-call spend ledger, oldest first — one {@link SpendEvent} per priced\n * `record()` / `settle()` / `recordTool()` / `settleTool()`. Returns a\n * snapshot copy: mutating it cannot corrupt the ledger.\n */\n get spendLog(): SpendEvent[] {\n return [...this.spendEvents];\n }\n\n /**\n * The spend ledger as JSONL — one event per line, newline-terminated.\n *\n * The schema is stable and language-independent (snake_case keys, fixed order;\n * optional fields omitted when absent), identical to the Python package's\n * `export_log()`, so heterogeneous agents produce logs you can concatenate and\n * analyse as one stream. (The *schema* is the contract, not the bytes: the two\n * runtimes may render the same float differently, e.g. JS `0.0000025` vs\n * Python `2.5e-06`.) Empty ledger yields `\"\"`.\n */\n exportLog(): string {\n return this.spendEvents\n .map((e) => {\n // snake_case wire shape, fixed key order — the cross-language schema.\n const row: Record<string, unknown> = {\n timestamp: e.timestamp,\n kind: e.kind,\n model_or_tool: e.modelOrTool,\n prompt_tokens: e.promptTokens,\n completion_tokens: e.completionTokens,\n cost_usd: e.costUsd,\n };\n if (e.label !== undefined) row.label = e.label;\n if (e.reserved !== undefined) row.reserved = e.reserved;\n return `${JSON.stringify(row)}\\n`;\n })\n .join(\"\");\n }\n\n /**\n * **Opt in** to pushing this guard's ledger to Floe's Reconcile Mode.\n *\n * Off by default — this is the *only* way to turn sync on, and it turns on\n * nothing else: it stores your key/endpoint and does **not** send. Nothing\n * leaves the process until you call {@link BudgetGuard.sync}. Zero-telemetry\n * stays the default; there is no background send.\n *\n * Why: Floe's gateway can't see spend it never routed (BYOK, self-hosted,\n * off-path). Syncing your local ledger is what makes that spend — and your\n * **Coverage Score** — visible. Budget, not balance: it reports what you already\n * spent for coverage/attribution; it moves no money.\n *\n * What will leave the process when you call {@link BudgetGuard.sync}: exactly the\n * {@link BudgetGuard.exportLog} JSONL (priced spend events — no prompts, no\n * content, no identifiers beyond a `label` you set).\n *\n * @param apiKey Floe agent key (`floe_<hex>`, `read_write`). Defaults to the\n * `FLOE_API_KEY` env var at `sync()` time.\n * @param options.baseUrl API base. Defaults to `FLOE_API_BASE_URL`, else prod.\n *\n * Revoke with {@link BudgetGuard.disableSync}; the guard sends nothing after.\n */\n enableSync(apiKey?: string, options: { baseUrl?: string } = {}): void {\n this.syncEnabled = true;\n this.syncKey = apiKey ?? null;\n this.syncBase = options.baseUrl ?? null;\n }\n\n /**\n * Turn ledger sync back off (revoke the opt-in). The guard sends nothing after\n * this; {@link BudgetGuard.sync} throws until {@link BudgetGuard.enableSync} is\n * called again.\n */\n disableSync(): void {\n this.syncEnabled = false;\n this.syncKey = null;\n this.syncBase = null;\n }\n\n /**\n * Push the current spend ledger to Reconcile Mode; resolve with events accepted.\n *\n * Requires {@link BudgetGuard.enableSync} first — this is the explicit send, the\n * only thing that transmits the ledger. Throws if sync isn't enabled (so a guard\n * that never opted in can never send). An empty ledger is a no-op (`0`, no\n * network). Re-syncing is safe: the server is idempotent, so already-ingested\n * events are not double-counted.\n *\n * @throws {Error} sync was not enabled (call {@link BudgetGuard.enableSync} first).\n * @throws {LedgerSyncError} the opt-in send failed (missing key, non-2xx, network).\n */\n async sync(): Promise<number> {\n // Snapshot opt-in state + key/base synchronously, before any await: a\n // disableSync() interleaved on a later microtask can't clear the key mid-call\n // (which would silently fall back to $FLOE_API_KEY) or let an upload start\n // after revocation. A disable happens-before (we throw) or happens-after (we\n // send the key we captured while still enabled).\n const enabled = this.syncEnabled;\n const key = this.syncKey;\n const base = this.syncBase;\n if (!enabled) {\n throw new Error(\n \"ledger sync is not enabled — call guard.enableSync(apiKey) first. \" +\n \"Sync is opt-in and off by default (zero-telemetry).\",\n );\n }\n return pushLedger(this.exportLog(), key ?? undefined, { baseUrl: base ?? undefined });\n }\n\n /**\n * The default next-call prediction when the caller supplies no estimate.\n * Conservative: the costlier of the last LLM call and the last tool call — a\n * mixed loop predicts the pricier kind, which at worst blocks one call early\n * (fail-closed) rather than letting a crossing call through because the LAST\n * event happened to be cheap.\n */\n private defaultEstimate(): number {\n return Math.max(this.lastLlmCost, this.lastToolCost);\n }\n\n /**\n * Subtract a settled/released hold from the in-flight tally. A handle larger\n * than EVERYTHING currently held cannot have come from a matching\n * `reserve()` — throwing beats silently clamping, which would free OTHER\n * callers' holds and fail the ceiling open. The epsilon absorbs float dust\n * from accumulating and draining many holds; per-caller over-release (a\n * handle within the total but larger than the caller's own hold) is\n * undetectable without per-handle tracking and remains the caller's\n * responsibility.\n */\n private consumeReservation(reserved: ReservationHandle): void {\n const usd = isReservation(reserved) ? reserved.usd : reserved;\n const tokens = isReservation(reserved) ? reserved.tokens : 0;\n if (usd > this.reserved + EPS) {\n throw new RangeError(\n `reserved handle (${usd}) exceeds total in-flight reservations ` +\n `(${this.reserved}) — a handle must come from a matching reserve()`,\n );\n }\n if (tokens > this.reservedTokens) {\n throw new RangeError(\n `reserved token handle (${tokens}) exceeds total in-flight token ` +\n `reservations (${this.reservedTokens}) — a handle must come from a matching reserve()`,\n );\n }\n this.reserved = Math.max(0, this.reserved - usd);\n this.reservedTokens = Math.max(0, this.reservedTokens - tokens);\n // Drain the innermost active step too (mirrors the aggregate drain). A\n // handle is drained against whatever step is innermost now — the\n // sequential-loop contract; concurrent parallel steps are out of scope (#46).\n const step = this.steps.length ? this.steps[this.steps.length - 1] : null;\n if (step !== null) {\n step.reservedUsd = Math.max(0, step.reservedUsd - usd);\n step.reservedTokens = Math.max(0, step.reservedTokens - tokens);\n }\n }\n\n /**\n * The USD amount of a handle, validated. Both shapes are checked — a raw\n * number and a {@link BudgetReservation}'s `usd`/`tokens` fields — so a bad\n * hand-rolled handle can't corrupt the in-flight tally.\n */\n /**\n * Validate a caller-supplied token estimate. Rejects a fraction, NaN,\n * Infinity, or boolean (via `Number.isInteger`) so it can't disable the\n * integer token hard-stops or corrupt the in-flight token tally. `undefined`\n * (the default) is fine; a negative integer is clamped by `Math.max(0, ...)`,\n * matching the lenient USD estimate. Mirrors Python's `_validate_token_estimate`.\n */\n private validateTokenEstimate(estimatedTokens: number | undefined): void {\n if (estimatedTokens !== undefined && !Number.isInteger(estimatedTokens)) {\n throw new RangeError(`estimatedTokens must be an integer, got ${estimatedTokens}`);\n }\n }\n\n private reservedUsdOf(reserved: ReservationHandle): number {\n if (isReservation(reserved)) {\n // A BudgetReservation is public (re-exported), so validate its fields here\n // — a hand-rolled { usd: NaN, tokens: -1 } must not slip past into\n // consumeReservation and corrupt the in-flight tallies. Mirrors Python's\n // BudgetReservation.__post_init__.\n if (!Number.isFinite(reserved.usd) || reserved.usd < 0) {\n throw new RangeError(\n `reserved.usd must be a finite, non-negative number, got ${reserved.usd}`,\n );\n }\n if (!Number.isInteger(reserved.tokens) || reserved.tokens < 0) {\n throw new RangeError(\n `reserved.tokens must be a non-negative integer, got ${reserved.tokens}`,\n );\n }\n return reserved.usd;\n }\n if (!Number.isFinite(reserved) || reserved < 0) {\n throw new RangeError(`reserved must be a finite, non-negative number, got ${reserved}`);\n }\n return reserved;\n }\n\n /**\n * Accrue a settled call into the innermost active step (no-op when none).\n * Sequential-loop contract: the innermost step owns the call.\n */\n private accrueStep(cost: number, tokens: number): void {\n const step = this.steps.length ? this.steps[this.steps.length - 1] : null;\n if (step !== null) {\n step.spentUsd += cost;\n step.spentTokens += tokens;\n }\n }\n\n /**\n * The ONE choke point, across two dimensions and two scopes. Returns the first\n * ceiling that blocks as `[dimension, scope, spent, limit]` — dimension\n * `\"usd\" | \"tokens\"`, scope `\"aggregate\" | \"step\"` — or `null` if the call\n * fits everywhere. Aggregate is checked before step (the guard-wide ceiling\n * is the hard limit). `spent` / `limit` are captured here so `raiseBlock`\n * (called outside the lock) reads no shared state and stays race-free.\n */\n private blockingCross(\n estimateUsd: number,\n estimateTokens: number,\n ): [\"usd\" | \"tokens\", \"aggregate\" | \"step\", number, number] | null {\n // Aggregate USD — same comparison the original check/reserve used.\n const committed = this.spentUsd + this.reserved;\n if (committed > this.limitUsd - EPS || committed + estimateUsd > this.limitUsd + EPS) {\n return [\"usd\", \"aggregate\", this.spentUsd, this.limitUsd];\n }\n // Aggregate tokens (integers — no epsilon).\n if (this.tokenLimit !== null) {\n const committedT = this.spentTokens + this.reservedTokens;\n if (committedT >= this.tokenLimit || committedT + estimateTokens > this.tokenLimit) {\n return [\"tokens\", \"aggregate\", committedT, this.tokenLimit];\n }\n }\n // Innermost active step's caps (crossing an outer step requires first\n // crossing the inner one, so the innermost is sufficient).\n const step = this.steps.length ? this.steps[this.steps.length - 1] : null;\n if (step !== null) {\n if (step.maxUsd !== null) {\n const sCommitted = step.spentUsd + step.reservedUsd;\n if (sCommitted > step.maxUsd - EPS || sCommitted + estimateUsd > step.maxUsd + EPS) {\n // Report the step's own committed spend and ceiling so BudgetExceeded\n // names the actual violated boundary, not the aggregate values.\n return [\"usd\", \"step\", sCommitted, step.maxUsd];\n }\n }\n if (step.maxTokens !== null) {\n const sCommittedT = step.spentTokens + step.reservedTokens;\n if (sCommittedT >= step.maxTokens || sCommittedT + estimateTokens > step.maxTokens) {\n return [\"tokens\", \"step\", sCommittedT, step.maxTokens];\n }\n }\n }\n return null;\n }\n\n /** Notify + throw the right error for a [dimension, scope, spent, limit] block. */\n private raiseBlock(blocked: [\"usd\" | \"tokens\", \"aggregate\" | \"step\", number, number]): never {\n const [dimension, scope, spent, limit] = blocked;\n if (dimension === \"usd\") {\n this.onBlock(spent, limit);\n throw new BudgetExceeded(spent, limit);\n }\n // Token block — counts came from blockingCross; on_block is dollar-shaped\n // so a token block skips it.\n throw new TokenBudgetExceeded(Math.trunc(spent), Math.trunc(limit), scope);\n }\n\n /**\n * Scope a per-step USD and/or token cap for a **sequential agent loop**.\n *\n * Runs `fn` with a step pushed onto the guard's stack; the\n * enforcement/accrual path honours the innermost active step *on top of* the\n * aggregate ceilings. A call that would cross the step's `maxUsd` / `maxTokens`\n * is hard-blocked ({@link BudgetExceeded} / {@link TokenBudgetExceeded} with\n * `scope: \"step\"`) even if the aggregate budget has room. `fn` receives the\n * SAME guard, so no adapter needs to know about steps. The step is popped when\n * `fn` settles or throws. **Not for concurrent parallel steps on one guard** —\n * that's out of scope for issue #46; use one guard per parallel branch.\n */\n step<T>(\n options: { maxUsd?: number; maxTokens?: number },\n fn: (guard: BudgetGuard) => T,\n ): T {\n const { maxUsd, maxTokens } = options;\n if (maxUsd !== undefined && (!Number.isFinite(maxUsd) || maxUsd < 0)) {\n throw new RangeError(`maxUsd must be a finite, non-negative number, got ${maxUsd}`);\n }\n if (maxTokens !== undefined && (!Number.isInteger(maxTokens) || maxTokens < 0)) {\n throw new RangeError(`maxTokens must be a non-negative integer, got ${maxTokens}`);\n }\n const state: StepState = {\n maxUsd: maxUsd ?? null,\n maxTokens: maxTokens ?? null,\n spentUsd: 0,\n spentTokens: 0,\n reservedUsd: 0,\n reservedTokens: 0,\n };\n this.steps.push(state);\n // Pop our own state; splice by identity so an out-of-order exit can't\n // silently drop someone else's step.\n const pop = (): void => {\n const idx = this.steps.lastIndexOf(state);\n if (idx !== -1) this.steps.splice(idx, 1);\n };\n let result: T;\n try {\n result = fn(this);\n } catch (err) {\n pop();\n throw err;\n }\n // An async `fn` returns a promise synchronously — popping now would disable\n // step enforcement across every `await` inside it (the common agent-loop\n // shape). Keep the step on the stack until the promise settles, then pop.\n if (result != null && typeof (result as { then?: unknown }).then === \"function\") {\n return (result as unknown as Promise<unknown>).finally(pop) as unknown as T;\n }\n pop();\n return result;\n }\n\n private appendEvent(event: SpendEvent): void {\n // Frozen for parity with Python's frozen dataclass: spendLog copies the\n // array but shares the event objects, so an unfrozen event would let a\n // consumer silently rewrite logged history.\n this.spendEvents.push(Object.freeze(event));\n if (this.maxLogEvents !== undefined && this.spendEvents.length > this.maxLogEvents) {\n // Ring buffer: drop the oldest overflow (at most one per append).\n this.spendEvents.splice(0, this.spendEvents.length - this.maxLogEvents);\n }\n }\n\n /**\n * Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.\n *\n * `nearLimit` flips once utilization reaches `nearLimitBps` (default 80%), so an\n * agent can taper *before* the hard-stop. Advisory only: read it to adapt;\n * {@link BudgetGuard.check} is what enforces the ceiling.\n */\n advisory(): BudgetAdvisory {\n // Floor (not round) so usedBps never over-reports utilization and nearLimit\n // flips exactly when the threshold is reached; the epsilon absorbs float noise\n // and Math.floor matches Python's int() exactly (round() would diverge).\n const usedBps =\n this.limitUsd <= 0\n ? 10000\n : Math.max(0, Math.min(10000, Math.floor((this.spentUsd / this.limitUsd) * 10000 + 1e-9)));\n const remainingUsd = Math.max(0, this.limitUsd - this.spentUsd);\n // The costlier of the last LLM and tool call — the guard's own default\n // reservation estimate; 0 before any call is recorded.\n const expectedCost = Math.max(this.lastLlmCost, this.lastToolCost);\n // +1e-9 absorbs float noise so e.g. 0.6/0.2 floors to 3 not 2 (same epsilon\n // rationale as usedBps above, and keeps Python int() parity).\n const estCallsRemaining =\n expectedCost > 0 ? Math.floor(remainingUsd / expectedCost + 1e-9) : null;\n // Burn rate: spend ÷ minutes elapsed since the guard was created. null until\n // any wall-clock time has passed (avoids a divide-by-zero at t0); $0 spent over\n // real elapsed time is a legitimate 0/min, not null.\n const elapsedMin = (Date.now() - this.createdAtMs) / 60000;\n const burnRateUsdPerMin = elapsedMin > 0 ? this.spentUsd / elapsedMin : null;\n // Aggregate token utilization, mirroring usedBps. null (no signal) when the\n // token dimension is unset — a pure USD guard's advisory is unchanged.\n let tokenUsedBps: number | null = null;\n let remainingTokens: number | null = null;\n let nearToken = false;\n if (this.tokenLimit !== null) {\n tokenUsedBps =\n this.tokenLimit <= 0\n ? 10000\n : Math.max(\n 0,\n Math.min(10000, Math.floor((this.spentTokens / this.tokenLimit) * 10000 + 1e-9)),\n );\n remainingTokens = Math.max(0, this.tokenLimit - this.spentTokens);\n nearToken = tokenUsedBps >= this.nearLimitBps;\n }\n // Innermost active step's headroom + nearness (per dimension, only when\n // capped). null keeps the fields absent for old readers.\n const step = this.steps.length ? this.steps[this.steps.length - 1] : null;\n let stepRemainingUsd: number | null = null;\n let stepRemainingTokens: number | null = null;\n let nearStep = false;\n if (step !== null) {\n if (step.maxUsd !== null) {\n stepRemainingUsd = Math.max(0, step.maxUsd - step.spentUsd);\n if (step.maxUsd <= 0) {\n nearStep = true;\n } else {\n const sBps = Math.floor((step.spentUsd / step.maxUsd) * 10000 + 1e-9);\n nearStep = nearStep || sBps >= this.nearLimitBps;\n }\n }\n if (step.maxTokens !== null) {\n stepRemainingTokens = Math.max(0, step.maxTokens - step.spentTokens);\n if (step.maxTokens <= 0) {\n nearStep = true;\n } else {\n const sTBps = Math.floor((step.spentTokens / step.maxTokens) * 10000 + 1e-9);\n nearStep = nearStep || sTBps >= this.nearLimitBps;\n }\n }\n }\n return {\n // nearLimit now also flips when a token ceiling or the active step is near\n // its cap, so a router can downshift before ANY hard-stop.\n nearLimit: usedBps >= this.nearLimitBps || nearToken || nearStep,\n usedBps,\n // Settled budget: limit minus accrued spend, deliberately NOT net of\n // in-flight reservations. Unlike the remainingUsd getter (which subtracts\n // `reserved`), the advisory is a soft utilization signal about money already\n // spent, while the getter reports what a new call can still claim.\n remainingUsd,\n limitUsd: this.limitUsd,\n spentUsd: this.spentUsd,\n scope: \"local\",\n expectedCost,\n estCallsRemaining,\n tokenUsedBps,\n remainingTokens,\n stepRemainingUsd,\n stepRemainingTokens,\n burnRateUsdPerMin,\n };\n }\n}\n\nfunction defaultOnBlock(spentUsd: number, limitUsd: number): void {\n console.error(\n \"BUDGET EXCEEDED — call blocked\\n\" +\n ` spent so far: $${spentUsd.toFixed(6)} | ceiling: $${limitUsd.toFixed(6)}\\n` +\n \" The next call would cross your budget; floe-guard stopped your agent \" +\n \"before it ran.\",\n );\n}\n","/**\n * LatencyBudget — a cumulative tool-chain deadline, sibling to BudgetGuard.\n *\n * BudgetGuard stops an agent before its next call crosses a USD ceiling;\n * LatencyBudget stops it before the next call would blow an end-user SLA:\n *\n * ```ts\n * const deadline = new LatencyBudget(5000);\n * ...\n * deadline.check(800); // throws DeadlineExceeded when projected over\n * if (deadline.advisory().nearDeadline) useFasterModel();\n * router.pick({ maxLatencyMs: deadline.remainingMs });\n * ```\n *\n * Design notes (mirroring `src/floe_guard/latency.py`):\n * - **Monotonic clock** — `performance.now()`, never wall time.\n * - **Cooperative, not preemptive** — the guard provides the deadline signal;\n * aborting a stalled in-flight call is the framework's job (AbortSignal).\n * `check()` prevents the NEXT call from starting.\n * - **Advisory symmetry** — `nearDeadline` / `usedBps` / `remainingMs` are the\n * latency twin of BudgetGuard's `nearLimit` / `usedBps` / `remainingUsd`.\n * - **In-process scope** — one instance per request/run; distributed latency\n * tracking is out of scope.\n */\n\nimport { DeadlineExceeded } from \"./errors.js\";\n\n/** A context-aware deadline signal — the latency twin of {@link BudgetAdvisory}.\n * Soft by design; the hard-stop is {@link LatencyBudget.check}. */\nexport interface LatencyAdvisory {\n nearDeadline: boolean;\n /** SLA consumed, basis points 0..10000 (8500 = 85%). */\n usedBps: number;\n remainingMs: number;\n slaMs: number;\n elapsedMs: number;\n}\n\nexport interface LatencyBudgetOptions {\n /**\n * Utilization (basis points, 0..10000) at which {@link LatencyBudget.advisory}\n * flags `nearDeadline` so an agent can downshift to a faster path before the\n * wall. Default 8000 (80%), matching BudgetGuard's `nearLimitBps`.\n */\n nearDeadlineBps?: number;\n /** Invoked with `(elapsedMs, slaMs)` right before {@link DeadlineExceeded} is thrown. */\n onBlock?: (elapsedMs: number, slaMs: number) => void;\n /** Milliseconds-returning monotonic clock, injectable for tests. Defaults to `performance.now`. */\n clock?: () => number;\n}\n\nexport class LatencyBudget {\n readonly slaMs: number;\n readonly nearDeadlineBps: number;\n private readonly onBlock?: (elapsedMs: number, slaMs: number) => void;\n private readonly clock: () => number;\n private readonly startedAt: number;\n\n /** The budget starts counting at construction — build it when the request\n * (and its SLA) starts. */\n constructor(slaMs: number, options: LatencyBudgetOptions = {}) {\n if (!(Number.isFinite(slaMs) && slaMs > 0)) {\n throw new RangeError(\"slaMs must be > 0\");\n }\n const nearDeadlineBps = options.nearDeadlineBps ?? 8000;\n if (!Number.isInteger(nearDeadlineBps) || nearDeadlineBps < 0 || nearDeadlineBps > 10000) {\n throw new RangeError(\"nearDeadlineBps must be an integer 0..10000\");\n }\n this.slaMs = slaMs;\n this.nearDeadlineBps = nearDeadlineBps;\n this.onBlock = options.onBlock;\n this.clock = options.clock ?? (() => performance.now());\n this.startedAt = this.clock();\n }\n\n /** Milliseconds since construction (monotonic). */\n get elapsedMs(): number {\n return this.clock() - this.startedAt;\n }\n\n /** Milliseconds left before the SLA, floored at 0 — the readable signal a\n * router uses to pick a faster fallback or truncate work mid-chain. */\n get remainingMs(): number {\n return Math.max(0, this.slaMs - this.elapsedMs);\n }\n\n /**\n * Throw {@link DeadlineExceeded} when the projected elapsed time (now +\n * `expectedMs` for the upcoming call) would blow the SLA. Call it\n * immediately before each tool/model call; pass 0 to only gate on time\n * already spent.\n */\n check(expectedMs = 0): void {\n if (!(Number.isFinite(expectedMs) && expectedMs >= 0)) {\n throw new RangeError(\"expectedMs must be >= 0\");\n }\n const elapsed = this.elapsedMs;\n if (elapsed + expectedMs > this.slaMs) {\n this.onBlock?.(elapsed, this.slaMs);\n throw new DeadlineExceeded(elapsed, this.slaMs);\n }\n }\n\n /** The soft near-deadline signal — symmetric to `BudgetGuard.advisory()`. */\n advisory(): LatencyAdvisory {\n const elapsed = this.elapsedMs;\n // round (not floor) — matches the Python twin; float clock arithmetic can\n // land a hair under an exact boundary.\n const usedBps = elapsed > 0 ? Math.min(10000, Math.round((elapsed * 10000) / this.slaMs)) : 0;\n return {\n nearDeadline: usedBps >= this.nearDeadlineBps,\n usedBps,\n remainingMs: Math.max(0, this.slaMs - elapsed),\n slaMs: this.slaMs,\n elapsedMs: elapsed,\n };\n }\n}\n","/**\n * Vercel AI SDK middleware that enforces a {@link BudgetGuard} in the call path.\n *\n * This is the TypeScript counterpart to the Python framework adapters. The AI SDK\n * is TypeScript-only, so it ships as its own npm package.\n *\n * Works with BOTH `ai@4` (`LanguageModelV1Middleware`) and `ai@5`\n * (`LanguageModelV2Middleware`). The two majors renamed the middleware type and\n * the usage fields (`promptTokens`/`completionTokens` → `inputTokens`/\n * `outputTokens`), so this module deliberately imports nothing from `ai`: the\n * middleware is typed structurally against the surface both majors share, and\n * usage is read from whichever field pair the installed SDK reports.\n *\n * - `wrapGenerate({ doGenerate, model })` — we `reserve()` (throws to hard-stop)\n * BEFORE calling `doGenerate()`, hold the reservation across the await, then\n * `settle()` from `result.usage`.\n * - `wrapStream({ doStream, model })` — we `reserve()` BEFORE `doStream()`, then\n * `settle()` from the `finish` part as the stream drains.\n *\n * Reserving before the await is what makes parallel calls (`Promise.all` over\n * several generations) honour the ceiling: each holds its slice instead of all\n * reading the same stale total (issue #18). The reservation is released if the\n * call throws, is cancelled by the consumer, or if a stream completes without a\n * usage-bearing finish part — in the last case the stream is also rejected with\n * an error rather than treated as a free ($0) call (fail-closed).\n *\n * The model id used for pricing comes from `model.modelId`.\n */\n\nimport type { BudgetGuard } from \"./guard.js\";\n\n/**\n * The middleware call surface shared by `ai@4` and `ai@5`. Both majors invoke\n * `wrapGenerate`/`wrapStream` with an options object carrying these members\n * (plus richer `model` fields we don't read). `doGenerate`/`doStream` are\n * declared optional so every 4.x/5.x minor's options type stays assignable —\n * the SDK always provides the one each hook actually calls.\n */\ninterface MiddlewareCallOptions {\n doGenerate?: () => PromiseLike<any>;\n doStream?: () => PromiseLike<any>;\n model: { modelId: string };\n params?: unknown;\n}\n\n/**\n * Structural stand-in for `LanguageModelV1Middleware` (ai@4) and\n * `LanguageModelV2Middleware` (ai@5) — assignable to the `middleware` option of\n * `wrapLanguageModel` on either major.\n */\nexport interface BudgetGuardMiddleware {\n wrapGenerate: (options: MiddlewareCallOptions) => Promise<any>;\n wrapStream: (options: MiddlewareCallOptions) => Promise<any>;\n}\n\n/**\n * Read prompt/completion token counts from an ai@4 usage object\n * (`promptTokens`/`completionTokens`) or an ai@5 one (`inputTokens`/\n * `outputTokens`). Throws when either count is missing or non-numeric: the guard\n * cannot meter spend it cannot see, and treating it as $0 would fail open.\n */\nfunction usageTokens(\n modelId: string,\n usage: unknown,\n): { promptTokens: number; completionTokens: number } {\n const u = usage as\n | {\n promptTokens?: unknown;\n completionTokens?: unknown;\n inputTokens?: unknown;\n outputTokens?: unknown;\n }\n | null\n | undefined;\n const promptTokens = u?.promptTokens ?? u?.inputTokens;\n const completionTokens = u?.completionTokens ?? u?.outputTokens;\n if (typeof promptTokens !== \"number\" || typeof completionTokens !== \"number\") {\n throw new Error(\n `Model '${modelId}' reported no token usage — the budget guard cannot ` +\n `meter spend it cannot see, so this call is rejected rather than ` +\n `treated as free.`,\n );\n }\n return { promptTokens, completionTokens };\n}\n\n/**\n * Build a budget-guard middleware that hard-stops the model before a call\n * crosses the guard's USD ceiling, and records priced token usage after.\n * Compatible with `wrapLanguageModel` from both `ai@4` and `ai@5`.\n *\n * @example\n * import { wrapLanguageModel } from \"ai\";\n * import { openai } from \"@ai-sdk/openai\";\n * import { BudgetGuard, budgetGuardMiddleware } from \"floe-guard\";\n *\n * const guard = new BudgetGuard(5.00);\n * const model = wrapLanguageModel({\n * model: openai(\"gpt-4o\"),\n * middleware: budgetGuardMiddleware(guard),\n * });\n */\nexport function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware {\n return {\n async wrapGenerate({ doGenerate, model }) {\n const reserved = guard.reserve(); // throws BudgetExceeded before the call runs\n let result: any;\n try {\n result = await doGenerate!();\n } catch (err) {\n guard.release(reserved); // the call failed before settle() took ownership\n throw err;\n }\n // From here settle() OWNS the reservation: it releases the hold on its own\n // throw (unpriceable / non-finite cost) and consumes it on success. Releasing\n // again would double-subtract and clear a concurrent call's in-flight hold.\n let handled = false;\n try {\n const { promptTokens, completionTokens } = usageTokens(\n model.modelId,\n result?.usage,\n );\n handled = true;\n guard.settle(model.modelId, promptTokens, completionTokens, { reserved });\n return result;\n } catch (err) {\n if (!handled) guard.release(reserved); // failed reading usage, before settle()\n throw err;\n }\n },\n\n async wrapStream({ doStream, model }) {\n const reserved = guard.reserve(); // throws BudgetExceeded before the stream starts\n let streamResult: any;\n try {\n streamResult = await doStream!();\n } catch (err) {\n guard.release(reserved); // the call failed before settle() took ownership\n throw err;\n }\n const { stream, ...rest } = streamResult as {\n stream: ReadableStream<any>;\n } & Record<string, unknown>;\n\n // `handled` flips once the reservation is disposed — settled on the finish\n // part, or released exactly once if the stream produced no usage (flush) or\n // ended early via error/cancellation (cancel). settle() owns disposal once\n // called, so nothing else may release after `handled` is set, or we'd\n // double-subtract and clear a concurrent call's hold.\n let handled = false;\n const guarded = stream.pipeThrough(\n new TransformStream<any, any>({\n transform(chunk, controller) {\n if (chunk?.type === \"finish\" && !handled) {\n try {\n const { promptTokens, completionTokens } = usageTokens(\n model.modelId,\n chunk.usage,\n );\n handled = true;\n guard.settle(model.modelId, promptTokens, completionTokens, { reserved });\n } catch (err) {\n // Release only if we never reached settle() (e.g. usage missing).\n // If settle() itself threw, it already released its own hold.\n if (!handled) {\n handled = true;\n guard.release(reserved);\n }\n throw err;\n }\n }\n controller.enqueue(chunk);\n },\n flush() {\n // The stream closed cleanly but never emitted a usage-bearing\n // finish part. Per the package's fail-closed philosophy (see\n // usageTokens()), we cannot treat an unmetered LLM call as free:\n // release the hold first (so the ceiling is not permanently\n // depleted), then throw so the consumer learns the call was not\n // settled. This mirrors how wrapGenerate() rejects missing usage.\n if (!handled) {\n handled = true;\n guard.release(reserved);\n throw new Error(\n `Model '${model.modelId}' stream completed without a token-usage ` +\n `finish part — the budget guard cannot meter spend it cannot see, ` +\n `so this call is rejected rather than treated as free.`,\n );\n }\n },\n cancel() {\n // Upstream error or consumer cancellation: flush() does not run here\n // (Web Streams: flush and cancel are mutually exclusive), so release\n // the still-held reservation.\n if (!handled) {\n handled = true;\n guard.release(reserved);\n }\n },\n // `cancel` is valid per the Streams spec and supported in Node 18+, but\n // TS's Transformer lib type lags and omits it — cast to keep the type check.\n } as Transformer<any, any>),\n );\n\n return { stream: guarded, ...rest };\n },\n };\n}\n","import { FloeGuardError } from \"./errors.js\";\nimport { type BudgetAdvisory, BudgetGuard } from \"./guard.js\";\n\nexport interface RetryPlan<T> {\n /** Operation to run for this retry attempt. */\n call: () => T | Promise<T>;\n /**\n * Estimated retry cost passed to `guard.check()` before the retry runs.\n * Leave undefined to use the guard's default last-call estimate.\n */\n estimatedCost?: number;\n}\n\nexport interface BudgetRetryOptions<T> {\n /** Estimated cost for retrying the original call. */\n estimatedCost?: number;\n /** Total attempts, including the first call. Default: 2. */\n maxAttempts?: number;\n /** Choose a cheaper retry plan when `guard.advisory().nearLimit` is true. */\n onDegrade?: (\n error: unknown,\n advisory: BudgetAdvisory,\n ) => RetryPlan<T> | Promise<RetryPlan<T> | undefined> | undefined;\n /**\n * Decide whether an error is retryable.\n *\n * Defaults to all non-{@link FloeGuardError} errors — every\n * {@link FloeGuardError} (including {@link BudgetExceeded} and\n * {@link UnpriceableModelError}) is treated as terminal because retrying a\n * deterministic guard error cannot fix it and would only multiply LLM spend.\n */\n retryIf?: (error: unknown) => boolean;\n}\n\n/**\n * Return `true` only for transient, non-{@link FloeGuardError} errors.\n *\n * Every {@link FloeGuardError} — including {@link BudgetExceeded} and\n * {@link UnpriceableModelError} — is considered terminal: retrying cannot fix\n * a deterministic guard error and would only multiply LLM spend.\n */\nfunction defaultRetryIf(error: unknown): boolean {\n return !(error instanceof FloeGuardError);\n}\n\n/**\n * Run `call` with budget-aware retries.\n *\n * The first attempt runs unchanged. If it fails with a retryable error, the\n * helper retries as-is with ample budget, asks `onDegrade` for a cheaper plan\n * when near the limit, and always calls `guard.check(estimatedCost)` before a\n * retry so an over-budget retry is blocked before it runs.\n */\nexport async function withBudgetRetry<T>(\n guard: BudgetGuard,\n call: () => T | Promise<T>,\n options: BudgetRetryOptions<T> = {},\n): Promise<T> {\n const maxAttempts = options.maxAttempts ?? 2;\n if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {\n throw new RangeError(`maxAttempts must be an integer >= 1, got ${maxAttempts}`);\n }\n const retryIf = options.retryIf ?? defaultRetryIf;\n let plan: RetryPlan<T> = { call, estimatedCost: options.estimatedCost };\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n try {\n return await plan.call();\n } catch (error) {\n if (attempt >= maxAttempts || !retryIf(error)) {\n throw error;\n }\n plan = await nextPlan(guard, error, plan, options.onDegrade);\n }\n }\n\n throw new Error(\"unreachable\");\n}\n\nasync function nextPlan<T>(\n guard: BudgetGuard,\n error: unknown,\n current: RetryPlan<T>,\n onDegrade: BudgetRetryOptions<T>[\"onDegrade\"],\n): Promise<RetryPlan<T>> {\n const advisory = guard.advisory();\n if (advisory.nearLimit && onDegrade !== undefined) {\n const degraded = await onDegrade(error, advisory);\n if (degraded !== undefined) {\n guard.check(degraded.estimatedCost);\n return degraded;\n }\n }\n guard.check(current.estimatedCost);\n return current;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "floe-guard",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "A local budget guardrail for AI agents — hard-stops your agent before its next LLM call crosses a USD ceiling. Vercel AI SDK middleware + LiveKit / Vapi / Retell voice adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cost_map.json
CHANGED
|
@@ -53,6 +53,18 @@
|
|
|
53
53
|
"litellm_provider": "anthropic",
|
|
54
54
|
"mode": "chat"
|
|
55
55
|
},
|
|
56
|
+
"claude-mythos-5": {
|
|
57
|
+
"input_cost_per_token": 0.00001,
|
|
58
|
+
"output_cost_per_token": 0.00005,
|
|
59
|
+
"litellm_provider": "anthropic",
|
|
60
|
+
"mode": "chat"
|
|
61
|
+
},
|
|
62
|
+
"claude-mythos-preview": {
|
|
63
|
+
"input_cost_per_token": 0.00001,
|
|
64
|
+
"output_cost_per_token": 0.00005,
|
|
65
|
+
"litellm_provider": "anthropic",
|
|
66
|
+
"mode": "chat"
|
|
67
|
+
},
|
|
56
68
|
"claude-opus-4-1": {
|
|
57
69
|
"input_cost_per_token": 0.000015,
|
|
58
70
|
"output_cost_per_token": 0.000075,
|
|
@@ -371,6 +383,12 @@
|
|
|
371
383
|
"litellm_provider": "gemini",
|
|
372
384
|
"mode": "chat"
|
|
373
385
|
},
|
|
386
|
+
"gemini-3.7-flash": {
|
|
387
|
+
"input_cost_per_token": 7.5e-7,
|
|
388
|
+
"output_cost_per_token": 0.00000375,
|
|
389
|
+
"litellm_provider": "gemini",
|
|
390
|
+
"mode": "chat"
|
|
391
|
+
},
|
|
374
392
|
"gemini-embedding-001": {
|
|
375
393
|
"input_cost_per_token": 1.5e-7,
|
|
376
394
|
"output_cost_per_token": 0,
|
|
@@ -449,6 +467,12 @@
|
|
|
449
467
|
"litellm_provider": "gemini",
|
|
450
468
|
"mode": "chat"
|
|
451
469
|
},
|
|
470
|
+
"gemini-robotics-er-2-streaming-preview": {
|
|
471
|
+
"input_cost_per_token": 0.000002,
|
|
472
|
+
"output_cost_per_token": 0.00001,
|
|
473
|
+
"litellm_provider": "gemini",
|
|
474
|
+
"mode": "chat"
|
|
475
|
+
},
|
|
452
476
|
"gpt-3.5-turbo": {
|
|
453
477
|
"input_cost_per_token": 5e-7,
|
|
454
478
|
"output_cost_per_token": 0.0000015,
|