patchwork-os 0.2.0-beta.10.canary.110 → 0.2.0-beta.10.canary.112

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.
@@ -46,7 +46,7 @@ export const HALT_CATEGORY_HINTS = {
46
46
  tool_threw: "check inner error in trace",
47
47
  tool_error: "check inner error in trace",
48
48
  kill_switch: "run `patchwork kill-switch release`",
49
- budget_exceeded: "raise tokensMax or shrink prompts",
49
+ budget_exceeded: "raise tokensMax / usdMax or shrink prompts",
50
50
  expect_failed: "inspect assertion vs actual output",
51
51
  step_timeout: "bump timeout_ms or speed up step",
52
52
  judge_revisions_exhausted: "raise max_revisions, refine the prompt, or set on_exhausted: proceed",
@@ -1 +1 @@
1
- {"version":3,"file":"haltCategory.js","sourceRoot":"","sources":["../../src/recipes/haltCategory.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"haltCategory.js","sourceRoot":"","sources":["../../src/recipes/haltCategory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Model price table — cost-aware routing Phase 2 (dormant data).
3
+ *
4
+ * A `(model id) → USD-per-million-tokens` table plus a loader. Nothing
5
+ * consumes it yet; Phase 3 wires `costUsd()` into RunBudget so `budget.usdMax`
6
+ * can be enforced for *measured* (API) drivers. Lands separately so the price
7
+ * data can be reviewed for accuracy on its own.
8
+ *
9
+ * Design notes (docs/design/cost-aware-routing.md):
10
+ * - Prices are DATA, not code, and OVERRIDABLE without a release:
11
+ * precedence env `PATCHWORK_PRICE_TABLE` (path) >
12
+ * `~/.patchwork/prices.json` >
13
+ * the built-in table below.
14
+ * - FAIL-OPEN everywhere: an unknown model, a malformed override, or a
15
+ * missing file never throws — `costUsd` just returns `undefined` (and the
16
+ * budget layer treats that as "unpriceable → don't enforce").
17
+ * - The built-in default lives here as a TS const (compiled into dist, so it
18
+ * is always available at runtime); overrides are JSON read from disk.
19
+ * - NO runtime network calls. Prices drift; refresh the const below and bump
20
+ * `_generatedAt`. `isPriceTableStale()` is provided for a scheduled check.
21
+ */
22
+ /** USD per 1,000,000 tokens, split by input vs output. */
23
+ export interface ModelPrice {
24
+ input: number;
25
+ output: number;
26
+ }
27
+ export interface PriceTableMeta {
28
+ /** ISO date (YYYY-MM-DD) the built-in prices were last reviewed. */
29
+ _generatedAt: string;
30
+ _unit: "usd_per_million_tokens";
31
+ _source: string;
32
+ _note: string;
33
+ /** Set by the loader when an override file was merged in. */
34
+ _override?: string;
35
+ }
36
+ export interface PriceTable {
37
+ _meta: PriceTableMeta;
38
+ /** Keyed by exact model id (e.g. "gpt-4o", "claude-haiku-4-5-20251001"). */
39
+ prices: Record<string, ModelPrice>;
40
+ }
41
+ /**
42
+ * Built-in default prices. **Approximate public list prices** (USD / 1M tokens)
43
+ * as of `_generatedAt` — VERIFY before relying on a USD cap; they drift, and a
44
+ * subscription/CLI driver's spend is notional, not real money out. Override via
45
+ * `~/.patchwork/prices.json` or `PATCHWORK_PRICE_TABLE`.
46
+ */
47
+ export declare const BUILTIN_PRICE_TABLE: PriceTable;
48
+ /**
49
+ * Load the effective price table: the built-in default with any override file
50
+ * merged over its `prices`. Fail-open — an unreadable / malformed override is
51
+ * ignored and the built-in is returned unchanged.
52
+ *
53
+ * Override file shape: `{ "prices": { "<model>": { "input": N, "output": N } } }`
54
+ * (a bare `{ "<model>": { ... } }` map is also accepted for convenience).
55
+ */
56
+ export declare function loadPriceTable(opts?: {
57
+ env?: NodeJS.ProcessEnv;
58
+ homeDir?: string;
59
+ }): PriceTable;
60
+ /** Price for one model, or undefined if the table does not list it. */
61
+ export declare function priceFor(model: string, table?: PriceTable): ModelPrice | undefined;
62
+ /**
63
+ * USD cost of a call given its token usage, or `undefined` when the model is
64
+ * not priced (fail-open: the budget layer then declines to enforce on it).
65
+ */
66
+ export declare function costUsd(model: string, usage: {
67
+ inputTokens: number;
68
+ outputTokens: number;
69
+ }, table?: PriceTable): number | undefined;
70
+ /**
71
+ * True when the table's `_generatedAt` is unparseable, in the future, or older
72
+ * than `maxAgeDays` (default 548 ≈ 18 months). Pure — caller supplies `now` —
73
+ * so a scheduled job can fail loudly without baking a wall-clock time-bomb into
74
+ * every PR's test run.
75
+ */
76
+ export declare function isPriceTableStale(generatedAt: string, now: number, maxAgeDays?: number): boolean;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Model price table — cost-aware routing Phase 2 (dormant data).
3
+ *
4
+ * A `(model id) → USD-per-million-tokens` table plus a loader. Nothing
5
+ * consumes it yet; Phase 3 wires `costUsd()` into RunBudget so `budget.usdMax`
6
+ * can be enforced for *measured* (API) drivers. Lands separately so the price
7
+ * data can be reviewed for accuracy on its own.
8
+ *
9
+ * Design notes (docs/design/cost-aware-routing.md):
10
+ * - Prices are DATA, not code, and OVERRIDABLE without a release:
11
+ * precedence env `PATCHWORK_PRICE_TABLE` (path) >
12
+ * `~/.patchwork/prices.json` >
13
+ * the built-in table below.
14
+ * - FAIL-OPEN everywhere: an unknown model, a malformed override, or a
15
+ * missing file never throws — `costUsd` just returns `undefined` (and the
16
+ * budget layer treats that as "unpriceable → don't enforce").
17
+ * - The built-in default lives here as a TS const (compiled into dist, so it
18
+ * is always available at runtime); overrides are JSON read from disk.
19
+ * - NO runtime network calls. Prices drift; refresh the const below and bump
20
+ * `_generatedAt`. `isPriceTableStale()` is provided for a scheduled check.
21
+ */
22
+ import { existsSync, readFileSync } from "node:fs";
23
+ import { homedir } from "node:os";
24
+ import { join } from "node:path";
25
+ /**
26
+ * Built-in default prices. **Approximate public list prices** (USD / 1M tokens)
27
+ * as of `_generatedAt` — VERIFY before relying on a USD cap; they drift, and a
28
+ * subscription/CLI driver's spend is notional, not real money out. Override via
29
+ * `~/.patchwork/prices.json` or `PATCHWORK_PRICE_TABLE`.
30
+ */
31
+ export const BUILTIN_PRICE_TABLE = {
32
+ _meta: {
33
+ _generatedAt: "2026-06-03",
34
+ _unit: "usd_per_million_tokens",
35
+ _source: "provider public list prices",
36
+ _note: "Approximate list prices (USD per 1M tokens). Review before relying on budget.usdMax; override via ~/.patchwork/prices.json or PATCHWORK_PRICE_TABLE.",
37
+ },
38
+ prices: {
39
+ // Anthropic (Claude 4.x)
40
+ "claude-opus-4-8": { input: 15, output: 75 },
41
+ "claude-sonnet-4-6": { input: 3, output: 15 },
42
+ "claude-haiku-4-5-20251001": { input: 1, output: 5 },
43
+ // OpenAI
44
+ "gpt-4o": { input: 2.5, output: 10 },
45
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
46
+ // xAI (Grok)
47
+ "grok-2-latest": { input: 2, output: 10 },
48
+ // Google (Gemini, API-keyed)
49
+ "gemini-2.5-pro": { input: 1.25, output: 10 },
50
+ "gemini-2.5-flash": { input: 0.3, output: 2.5 },
51
+ },
52
+ };
53
+ const MS_PER_DAY = 86_400_000;
54
+ /** Resolve the override file path per the precedence rule (or undefined). */
55
+ function resolveOverridePath(env, homeDir) {
56
+ const envPath = env.PATCHWORK_PRICE_TABLE;
57
+ if (typeof envPath === "string" && envPath.trim())
58
+ return envPath.trim();
59
+ const filePath = join(homeDir, ".patchwork", "prices.json");
60
+ return existsSync(filePath) ? filePath : undefined;
61
+ }
62
+ /** Extract a valid `{input, output}` price from an unknown override entry. */
63
+ function coercePrice(value) {
64
+ if (!value || typeof value !== "object")
65
+ return undefined;
66
+ const v = value;
67
+ if (typeof v.input === "number" && typeof v.output === "number") {
68
+ return { input: v.input, output: v.output };
69
+ }
70
+ return undefined;
71
+ }
72
+ /**
73
+ * Load the effective price table: the built-in default with any override file
74
+ * merged over its `prices`. Fail-open — an unreadable / malformed override is
75
+ * ignored and the built-in is returned unchanged.
76
+ *
77
+ * Override file shape: `{ "prices": { "<model>": { "input": N, "output": N } } }`
78
+ * (a bare `{ "<model>": { ... } }` map is also accepted for convenience).
79
+ */
80
+ export function loadPriceTable(opts = {}) {
81
+ const env = opts.env ?? process.env;
82
+ const homeDir = opts.homeDir ?? homedir();
83
+ const overridePath = resolveOverridePath(env, homeDir);
84
+ if (!overridePath)
85
+ return BUILTIN_PRICE_TABLE;
86
+ try {
87
+ const parsed = JSON.parse(readFileSync(overridePath, "utf8"));
88
+ const root = (parsed ?? {});
89
+ const rawPrices = root.prices && typeof root.prices === "object" ? root.prices : root;
90
+ // Null-prototype base so a user override keyed "__proto__" creates an own
91
+ // property instead of mutating the prototype (bracket-assign pollution).
92
+ const merged = Object.create(null);
93
+ Object.assign(merged, BUILTIN_PRICE_TABLE.prices);
94
+ for (const [model, entry] of Object.entries(rawPrices)) {
95
+ const price = coercePrice(entry);
96
+ if (price)
97
+ merged[model] = price;
98
+ }
99
+ return {
100
+ _meta: { ...BUILTIN_PRICE_TABLE._meta, _override: overridePath },
101
+ prices: merged,
102
+ };
103
+ }
104
+ catch {
105
+ // Malformed / unreadable override → fail open to the built-in table.
106
+ return BUILTIN_PRICE_TABLE;
107
+ }
108
+ }
109
+ /** Price for one model, or undefined if the table does not list it. */
110
+ export function priceFor(model, table = loadPriceTable()) {
111
+ // Own-property lookup only. A user-controlled model id that collides with an
112
+ // Object.prototype key ("__proto__", "constructor", "valueOf", "toString", …)
113
+ // must resolve to undefined (unpriced → fail open), NOT to an inherited
114
+ // prototype member — otherwise costUsd would compute on a function and return
115
+ // NaN, silently poisoning the USD budget (a prototype-walk bug class this
116
+ // codebase has been bitten by before — use Object.hasOwn, never `in`/bracket).
117
+ return Object.hasOwn(table.prices, model) ? table.prices[model] : undefined;
118
+ }
119
+ /**
120
+ * USD cost of a call given its token usage, or `undefined` when the model is
121
+ * not priced (fail-open: the budget layer then declines to enforce on it).
122
+ */
123
+ export function costUsd(model, usage, table = loadPriceTable()) {
124
+ const price = priceFor(model, table);
125
+ if (!price)
126
+ return undefined;
127
+ return ((usage.inputTokens / 1_000_000) * price.input +
128
+ (usage.outputTokens / 1_000_000) * price.output);
129
+ }
130
+ /**
131
+ * True when the table's `_generatedAt` is unparseable, in the future, or older
132
+ * than `maxAgeDays` (default 548 ≈ 18 months). Pure — caller supplies `now` —
133
+ * so a scheduled job can fail loudly without baking a wall-clock time-bomb into
134
+ * every PR's test run.
135
+ */
136
+ export function isPriceTableStale(generatedAt, now, maxAgeDays = 548) {
137
+ const t = Date.parse(generatedAt);
138
+ if (Number.isNaN(t))
139
+ return true;
140
+ if (t > now)
141
+ return true;
142
+ return now - t > maxAgeDays * MS_PER_DAY;
143
+ }
144
+ //# sourceMappingURL=priceTable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"priceTable.js","sourceRoot":"","sources":["../../../src/recipes/pricing/priceTable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAwBjC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAe;IAC7C,KAAK,EAAE;QACL,YAAY,EAAE,YAAY;QAC1B,KAAK,EAAE,wBAAwB;QAC/B,OAAO,EAAE,6BAA6B;QACtC,KAAK,EACH,sJAAsJ;KACzJ;IACD,MAAM,EAAE;QACN,yBAAyB;QACzB,iBAAiB,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;QAC5C,mBAAmB,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE;QAC7C,2BAA2B,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;QACpD,SAAS;QACT,QAAQ,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;QACpC,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;QAC3C,aAAa;QACb,eAAe,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE;QACzC,6BAA6B;QAC7B,gBAAgB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE;QAC7C,kBAAkB,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE;KAChD;CACF,CAAC;AAEF,MAAM,UAAU,GAAG,UAAU,CAAC;AAE9B,6EAA6E;AAC7E,SAAS,mBAAmB,CAC1B,GAAsB,EACtB,OAAe;IAEf,MAAM,OAAO,GAAG,GAAG,CAAC,qBAAqB,CAAC;IAC1C,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE;QAAE,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;IACzE,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;IAC5D,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;AACrD,CAAC;AAED,8EAA8E;AAC9E,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC1D,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAChE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;IAC9C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAsD,EAAE;IAExD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC;IAC1C,MAAM,YAAY,GAAG,mBAAmB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACvD,IAAI,CAAC,YAAY;QAAE,OAAO,mBAAmB,CAAC;IAE9C,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;QACvE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,CAA4B,CAAC;QACvD,MAAM,SAAS,GACb,IAAI,CAAC,MAAM,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QAEtE,0EAA0E;QAC1E,yEAAyE;QACzE,MAAM,MAAM,GAA+B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC/D,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;QAClD,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CACzC,SAAoC,CACrC,EAAE,CAAC;YACF,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;YACjC,IAAI,KAAK;gBAAE,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;QACnC,CAAC;QACD,OAAO;YACL,KAAK,EAAE,EAAE,GAAG,mBAAmB,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE;YAChE,MAAM,EAAE,MAAM;SACf,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,qEAAqE;QACrE,OAAO,mBAAmB,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,QAAQ,CACtB,KAAa,EACb,QAAoB,cAAc,EAAE;IAEpC,6EAA6E;IAC7E,8EAA8E;IAC9E,wEAAwE;IACxE,8EAA8E;IAC9E,0EAA0E;IAC1E,+EAA+E;IAC/E,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC9E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CACrB,KAAa,EACb,KAAoD,EACpD,QAAoB,cAAc,EAAE;IAEpC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,OAAO,CACL,CAAC,KAAK,CAAC,WAAW,GAAG,SAAS,CAAC,GAAG,KAAK,CAAC,KAAK;QAC7C,CAAC,KAAK,CAAC,YAAY,GAAG,SAAS,CAAC,GAAG,KAAK,CAAC,MAAM,CAChD,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,WAAmB,EACnB,GAAW,EACX,UAAU,GAAG,GAAG;IAEhB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAClC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,CAAC,GAAG,GAAG;QAAE,OAAO,IAAI,CAAC;IACzB,OAAO,GAAG,GAAG,CAAC,GAAG,UAAU,GAAG,UAAU,CAAC;AAC3C,CAAC"}
@@ -1,30 +1,29 @@
1
1
  /**
2
- * Per-recipe token budget — PR2b.
2
+ * Per-recipe budget — PR2b (tokens) + cost-routing Phase 3 (USD).
3
3
  *
4
- * Built on PR2a's `AgentResult.usage` plumbing. The runner constructs
5
- * one `RunBudget` per recipe execution; agent steps consult it before
6
- * dispatch (admission) and reconcile actual consumption after the call
7
- * returns. On breach the run halts at the *next* admission check (we
8
- * never retroactively fail a step that succeeded — the user's tokens
9
- * are already spent, halting after-the-fact would just confuse the
10
- * audit trail).
4
+ * Built on PR2a's `AgentResult.usage` plumbing. The runner constructs one
5
+ * `RunBudget` per recipe execution; agent steps consult it before dispatch
6
+ * (admission) and reconcile actual consumption after the call returns. On
7
+ * breach the run halts at the *next* admission check (we never retroactively
8
+ * fail a step that succeeded — the user's tokens/dollars are already spent;
9
+ * halting after-the-fact would just confuse the audit trail).
11
10
  *
12
- * Subscription drivers (Claude CLI, provider subprocess CLIs) don't
13
- * surface per-call token counts. `RunBudget` tracks which drivers were
14
- * unmeasured and exposes a deduped warnings list fail-open with a
15
- * single notice per driver per run, never block.
11
+ * Two independent caps, either or both:
12
+ * - `tokensMax` cumulative input + output tokens.
13
+ * - `usdMax` — cumulative USD, priced from token usage via the Phase 2
14
+ * price table (`costUsd`).
16
15
  *
17
- * Scope (PR2b only):
18
- * - cumulative token enforcement (input + output)
19
- * - halt-on-breach OR warn-on-breach via `BudgetPolicy.onBreach`
20
- * - subscription-driver fail-open with warning
21
- *
22
- * Out of scope (future):
23
- * - per-step caps (extend `BudgetPolicy` or add `AgentStep.tokensMax`)
24
- * - `usdMax` (needs a price table; subscription drivers complicate)
25
- * - `wallClockMs` (orthogonal to tokens, separate accounting)
16
+ * Subscription drivers (Claude CLI, provider subprocess CLIs) don't surface
17
+ * per-call token counts, and a model with no price-table entry can't be
18
+ * costed. Both cases FAIL OPEN: `RunBudget` records a deduped one-time
19
+ * warning and never blocks. So a USD cap enforces for *measured + priced*
20
+ * (API) drivers and is a no-op-with-notice for everything else — never a
21
+ * silent or a surprise halt on the token-blind default driver. (The opt-in
22
+ * estimate-the-unmeasured path is a planned follow-up; absent it, unmeasured
23
+ * = fail-open, which is the conservative default the design calls for.)
26
24
  */
27
25
  import type { AgentUsage } from "./agentExecutor.js";
26
+ import { type PriceTable } from "./pricing/priceTable.js";
28
27
  import type { BudgetPolicy } from "./schema.js";
29
28
  export interface BudgetAdmission {
30
29
  /** True = step may proceed. False = budget exhausted. */
@@ -36,35 +35,54 @@ export interface BudgetTotals {
36
35
  inputTokens: number;
37
36
  outputTokens: number;
38
37
  total: number;
39
- /** Remaining tokens before breach. Undefined when no limit is set. */
38
+ /** Remaining tokens before breach. Undefined when no token limit is set. */
40
39
  remaining?: number;
41
- /** True once total >= tokensMax. */
40
+ /** Cumulative measured USD. Undefined when no USD limit is set. */
41
+ usd?: number;
42
+ /** Remaining USD before breach. Undefined when no USD limit is set. */
43
+ usdRemaining?: number;
44
+ /** True once total tokens >= tokensMax (false when no token limit). */
42
45
  breached: boolean;
46
+ /** True once measured usd >= usdMax (false when no USD limit). */
47
+ usdBreached: boolean;
43
48
  /** Whether the configured policy halts on breach (vs warn). */
44
49
  haltOnBreach: boolean;
45
50
  }
46
51
  export declare class RunBudget {
47
52
  private readonly tokensMax?;
53
+ private readonly usdMax?;
48
54
  private readonly haltOnBreach;
55
+ private readonly priceTable?;
49
56
  private inputTokens;
50
57
  private outputTokens;
51
- /** Drivers that returned `usage: undefined` during this run, deduped. */
52
- private readonly unmeasuredDrivers;
58
+ private usdSpent;
59
+ /** Dedup keys for one-time warnings (unmeasured driver, breach, etc.). */
60
+ private readonly warnedKeys;
53
61
  /** Free-form warnings surfaced via the run log. */
54
62
  private readonly warningList;
55
- constructor(policy?: BudgetPolicy);
63
+ /**
64
+ * @param policy recipe budget block.
65
+ * @param priceTable optional injected table (tests); otherwise loaded once
66
+ * here when a USD cap is set (file/env override aware).
67
+ */
68
+ constructor(policy?: BudgetPolicy, priceTable?: PriceTable);
69
+ /** True when neither cap is configured — reconcile/admit are no-ops. */
70
+ private get hasBudget();
56
71
  /** Cheap admission check before dispatching an agent step. */
57
72
  admit(): BudgetAdmission;
58
73
  /**
59
- * Record the actual usage reported by an agent call. `usage` may be
60
- * undefined when the driver doesn't surface token counts — we record
61
- * the driver name once per run and continue (fail-open).
74
+ * Record the actual usage reported by an agent call. `usage` is undefined
75
+ * when the driver doesn't surface token counts — record the driver once and
76
+ * continue (fail-open). `model` is the resolved model (from `servedBy`) used
77
+ * to price USD; an unpriced model fails open with a one-time notice.
62
78
  */
63
- reconcile(driver: string, usage: AgentUsage | undefined): void;
79
+ reconcile(driver: string, usage: AgentUsage | undefined, model?: string): void;
64
80
  /** Snapshot of current totals + breach state. */
65
81
  totals(): BudgetTotals;
66
82
  /** Warnings collected so the runner can surface them in the run log. */
67
83
  warnings(): string[];
84
+ private pushOnce;
68
85
  private totalTokens;
69
- private breached;
86
+ private tokenBreached;
87
+ private usdMaxBreached;
70
88
  }
@@ -1,83 +1,125 @@
1
1
  /**
2
- * Per-recipe token budget — PR2b.
2
+ * Per-recipe budget — PR2b (tokens) + cost-routing Phase 3 (USD).
3
3
  *
4
- * Built on PR2a's `AgentResult.usage` plumbing. The runner constructs
5
- * one `RunBudget` per recipe execution; agent steps consult it before
6
- * dispatch (admission) and reconcile actual consumption after the call
7
- * returns. On breach the run halts at the *next* admission check (we
8
- * never retroactively fail a step that succeeded — the user's tokens
9
- * are already spent, halting after-the-fact would just confuse the
10
- * audit trail).
4
+ * Built on PR2a's `AgentResult.usage` plumbing. The runner constructs one
5
+ * `RunBudget` per recipe execution; agent steps consult it before dispatch
6
+ * (admission) and reconcile actual consumption after the call returns. On
7
+ * breach the run halts at the *next* admission check (we never retroactively
8
+ * fail a step that succeeded — the user's tokens/dollars are already spent;
9
+ * halting after-the-fact would just confuse the audit trail).
11
10
  *
12
- * Subscription drivers (Claude CLI, provider subprocess CLIs) don't
13
- * surface per-call token counts. `RunBudget` tracks which drivers were
14
- * unmeasured and exposes a deduped warnings list fail-open with a
15
- * single notice per driver per run, never block.
11
+ * Two independent caps, either or both:
12
+ * - `tokensMax` cumulative input + output tokens.
13
+ * - `usdMax` — cumulative USD, priced from token usage via the Phase 2
14
+ * price table (`costUsd`).
16
15
  *
17
- * Scope (PR2b only):
18
- * - cumulative token enforcement (input + output)
19
- * - halt-on-breach OR warn-on-breach via `BudgetPolicy.onBreach`
20
- * - subscription-driver fail-open with warning
21
- *
22
- * Out of scope (future):
23
- * - per-step caps (extend `BudgetPolicy` or add `AgentStep.tokensMax`)
24
- * - `usdMax` (needs a price table; subscription drivers complicate)
25
- * - `wallClockMs` (orthogonal to tokens, separate accounting)
16
+ * Subscription drivers (Claude CLI, provider subprocess CLIs) don't surface
17
+ * per-call token counts, and a model with no price-table entry can't be
18
+ * costed. Both cases FAIL OPEN: `RunBudget` records a deduped one-time
19
+ * warning and never blocks. So a USD cap enforces for *measured + priced*
20
+ * (API) drivers and is a no-op-with-notice for everything else — never a
21
+ * silent or a surprise halt on the token-blind default driver. (The opt-in
22
+ * estimate-the-unmeasured path is a planned follow-up; absent it, unmeasured
23
+ * = fail-open, which is the conservative default the design calls for.)
24
+ */
25
+ import { costUsd, loadPriceTable, } from "./pricing/priceTable.js";
26
+ /**
27
+ * Drivers that incur real, metered, per-token API billing — the ONLY ones a
28
+ * USD cap is enforced against. Subscription/CLI drivers (subprocess Claude/
29
+ * Gemini) report no usage and never reach the pricing path; `local`
30
+ * (self-hosted Ollama / LM Studio) DOES report usage but costs no real money,
31
+ * so it must not be priced at notional API rates and halted on spend that
32
+ * never happened. Anything not in this set fails open with a one-time notice.
26
33
  */
34
+ const BILLABLE_DRIVERS = new Set(["anthropic", "openai", "grok"]);
27
35
  export class RunBudget {
28
36
  tokensMax;
37
+ usdMax;
29
38
  haltOnBreach;
39
+ priceTable;
30
40
  inputTokens = 0;
31
41
  outputTokens = 0;
32
- /** Drivers that returned `usage: undefined` during this run, deduped. */
33
- unmeasuredDrivers = new Set();
42
+ usdSpent = 0;
43
+ /** Dedup keys for one-time warnings (unmeasured driver, breach, etc.). */
44
+ warnedKeys = new Set();
34
45
  /** Free-form warnings surfaced via the run log. */
35
46
  warningList = [];
36
- constructor(policy) {
47
+ /**
48
+ * @param policy recipe budget block.
49
+ * @param priceTable optional injected table (tests); otherwise loaded once
50
+ * here when a USD cap is set (file/env override aware).
51
+ */
52
+ constructor(policy, priceTable) {
37
53
  this.tokensMax = policy?.tokensMax;
54
+ this.usdMax = policy?.usdMax;
38
55
  this.haltOnBreach = (policy?.onBreach ?? "halt") === "halt";
56
+ if (this.usdMax !== undefined) {
57
+ this.priceTable = priceTable ?? loadPriceTable();
58
+ }
59
+ }
60
+ /** True when neither cap is configured — reconcile/admit are no-ops. */
61
+ get hasBudget() {
62
+ return this.tokensMax !== undefined || this.usdMax !== undefined;
39
63
  }
40
64
  /** Cheap admission check before dispatching an agent step. */
41
65
  admit() {
42
- if (this.tokensMax === undefined)
66
+ // warn-mode never blocks; the breach warning was emitted at reconcile.
67
+ if (!this.haltOnBreach)
43
68
  return { admitted: true };
44
- if (this.breached()) {
45
- if (!this.haltOnBreach) {
46
- // warn-mode: never block, just keep going. The warning was
47
- // already emitted on the post-call reconcile that breached.
48
- return { admitted: true };
49
- }
69
+ if (this.tokenBreached()) {
50
70
  return {
51
71
  admitted: false,
52
- reason: `Run exceeded its token budget — budget_exceeded: total=${this.totalTokens()} > tokensMax=${this.tokensMax}.`,
72
+ reason: `Run exceeded its token budget — budget_exceeded: total=${this.totalTokens()} >= tokensMax=${this.tokensMax}.`,
73
+ };
74
+ }
75
+ if (this.usdMaxBreached()) {
76
+ return {
77
+ admitted: false,
78
+ reason: `Run exceeded its USD budget — budget_exceeded: usd=$${this.usdSpent.toFixed(4)} >= usdMax=$${this.usdMax}.`,
53
79
  };
54
80
  }
55
81
  return { admitted: true };
56
82
  }
57
83
  /**
58
- * Record the actual usage reported by an agent call. `usage` may be
59
- * undefined when the driver doesn't surface token counts — we record
60
- * the driver name once per run and continue (fail-open).
84
+ * Record the actual usage reported by an agent call. `usage` is undefined
85
+ * when the driver doesn't surface token counts — record the driver once and
86
+ * continue (fail-open). `model` is the resolved model (from `servedBy`) used
87
+ * to price USD; an unpriced model fails open with a one-time notice.
61
88
  */
62
- reconcile(driver, usage) {
63
- if (this.tokensMax === undefined)
89
+ reconcile(driver, usage, model) {
90
+ if (!this.hasBudget)
64
91
  return;
65
92
  if (!usage) {
66
- if (!this.unmeasuredDrivers.has(driver)) {
67
- this.unmeasuredDrivers.add(driver);
68
- this.warningList.push(`Driver "${driver}" does not report token usage — budget enforcement skipped for its calls. Set recipe.budget.onBreach="warn" or move to an API driver to fix.`);
69
- }
93
+ this.pushOnce(`unmeasured:${driver}`, `Driver "${driver}" does not report token usage — budget enforcement skipped for its calls. Set recipe.budget.onBreach="warn" or move to an API driver to fix.`);
70
94
  return;
71
95
  }
72
96
  this.inputTokens += usage.inputTokens;
73
97
  this.outputTokens += usage.outputTokens;
74
- if (this.breached() && !this.haltOnBreach) {
75
- // warn-mode: emit a single in-band warning the first time we
76
- // cross the line. Subsequent steps continue running.
77
- const breachKey = "warn-breach-emitted";
78
- if (!this.unmeasuredDrivers.has(breachKey)) {
79
- this.unmeasuredDrivers.add(breachKey);
80
- this.warningList.push(`Token budget exceeded (total=${this.totalTokens()}, tokensMax=${this.tokensMax}) but onBreach="warn" — continuing.`);
98
+ if (this.usdMax !== undefined) {
99
+ if (!BILLABLE_DRIVERS.has(driver)) {
100
+ // Not a metered-API driver (local / subscription) — a USD cap here
101
+ // would be notional, not real money out. Fail open with a notice.
102
+ this.pushOnce(`notbilled:${driver}`, `Driver "${driver}" does not incur metered API cost — usdMax is not enforced for its calls.`);
103
+ }
104
+ else {
105
+ const cost = costUsd(model ?? "", usage, this.priceTable);
106
+ // `cost === undefined` (unpriced model) OR a non-finite cost (defends
107
+ // against a malformed price entry) → fail open, never poison usdSpent.
108
+ if (cost === undefined || !Number.isFinite(cost)) {
109
+ this.pushOnce(`unpriced:${model ?? "(no model)"}`, `Model "${model ?? "(unspecified)"}" is not in the price table — USD budget enforcement skipped for its calls. Add it to ~/.patchwork/prices.json or set the agent step's model.`);
110
+ }
111
+ else {
112
+ this.usdSpent += cost;
113
+ }
114
+ }
115
+ }
116
+ // warn-mode: emit a single in-band notice the first time we cross a cap.
117
+ if (!this.haltOnBreach) {
118
+ if (this.tokenBreached()) {
119
+ this.pushOnce("warn-token-breach", `Token budget exceeded (total=${this.totalTokens()}, tokensMax=${this.tokensMax}) but onBreach="warn" — continuing.`);
120
+ }
121
+ if (this.usdMaxBreached()) {
122
+ this.pushOnce("warn-usd-breach", `USD budget exceeded (usd=$${this.usdSpent.toFixed(4)}, usdMax=$${this.usdMax}) but onBreach="warn" — continuing.`);
81
123
  }
82
124
  }
83
125
  }
@@ -91,7 +133,12 @@ export class RunBudget {
91
133
  ...(this.tokensMax !== undefined && {
92
134
  remaining: Math.max(0, this.tokensMax - total),
93
135
  }),
94
- breached: this.breached(),
136
+ ...(this.usdMax !== undefined && {
137
+ usd: this.usdSpent,
138
+ usdRemaining: Math.max(0, this.usdMax - this.usdSpent),
139
+ }),
140
+ breached: this.tokenBreached(),
141
+ usdBreached: this.usdMaxBreached(),
95
142
  haltOnBreach: this.haltOnBreach,
96
143
  };
97
144
  }
@@ -99,11 +146,20 @@ export class RunBudget {
99
146
  warnings() {
100
147
  return [...this.warningList];
101
148
  }
149
+ pushOnce(key, msg) {
150
+ if (this.warnedKeys.has(key))
151
+ return;
152
+ this.warnedKeys.add(key);
153
+ this.warningList.push(msg);
154
+ }
102
155
  totalTokens() {
103
156
  return this.inputTokens + this.outputTokens;
104
157
  }
105
- breached() {
158
+ tokenBreached() {
106
159
  return this.tokensMax !== undefined && this.totalTokens() >= this.tokensMax;
107
160
  }
161
+ usdMaxBreached() {
162
+ return this.usdMax !== undefined && this.usdSpent >= this.usdMax;
163
+ }
108
164
  }
109
165
  //# sourceMappingURL=runBudget.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runBudget.js","sourceRoot":"","sources":["../../src/recipes/runBudget.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runBudget.js","sourceRoot":"","sources":["../../src/recipes/runBudget.ts"],"names":[],"mappings":"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"}