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.
- package/dist/recipes/haltCategory.js +1 -1
- package/dist/recipes/haltCategory.js.map +1 -1
- package/dist/recipes/pricing/priceTable.d.ts +76 -0
- package/dist/recipes/pricing/priceTable.js +144 -0
- package/dist/recipes/pricing/priceTable.js.map +1 -0
- package/dist/recipes/runBudget.d.ts +49 -31
- package/dist/recipes/runBudget.js +106 -50
- package/dist/recipes/runBudget.js.map +1 -1
- package/dist/recipes/schema.d.ts +15 -4
- package/dist/recipes/schemaGenerator.js +8 -2
- package/dist/recipes/schemaGenerator.js.map +1 -1
- package/dist/recipes/validation.js +45 -0
- package/dist/recipes/validation.js.map +1 -1
- package/dist/recipes/yamlRunner.js +24 -3
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/schemas/recipe.v1.json +8 -2
- package/package.json +1 -1
|
@@ -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":"AAAA;;;;;;;;;;;;GAYG;AA8CH;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAiC;IAChE,iBAAiB,EAAE,mBAAmB;IACtC,oBAAoB,EAAE,sBAAsB;IAC5C,WAAW,EAAE,aAAa;IAC1B,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;IACxB,WAAW,EAAE,qBAAqB;IAClC,eAAe,EAAE,iBAAiB;IAClC,aAAa,EAAE,eAAe;IAC9B,YAAY,EAAE,cAAc;IAC5B,yBAAyB,EAAE,2BAA2B;IACtD,YAAY,EAAE,cAAc;IAC5B,YAAY,EAAE,cAAc;IAC5B,aAAa,EAAE,eAAe;IAC9B,iBAAiB,EAAE,mBAAmB;IACtC,SAAS,EAAE,gBAAgB;IAC3B,OAAO,EAAE,eAAe;CACzB,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAiC;IAC/D,iBAAiB,EAAE,8BAA8B;IACjD,oBAAoB,EAAE,sCAAsC;IAC5D,WAAW,EAAE,gBAAgB;IAC7B,UAAU,EAAE,4BAA4B;IACxC,UAAU,EAAE,4BAA4B;IACxC,WAAW,EAAE,qCAAqC;IAClD,eAAe,EAAE,
|
|
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":"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"}
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-recipe
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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`
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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
|
|
86
|
+
private tokenBreached;
|
|
87
|
+
private usdMaxBreached;
|
|
70
88
|
}
|
|
@@ -1,83 +1,125 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-recipe
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
// warn-mode never blocks; the breach warning was emitted at reconcile.
|
|
67
|
+
if (!this.haltOnBreach)
|
|
43
68
|
return { admitted: true };
|
|
44
|
-
if (this.
|
|
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()}
|
|
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`
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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.
|
|
89
|
+
reconcile(driver, usage, model) {
|
|
90
|
+
if (!this.hasBudget)
|
|
64
91
|
return;
|
|
65
92
|
if (!usage) {
|
|
66
|
-
|
|
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.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"runBudget.js","sourceRoot":"","sources":["../../src/recipes/runBudget.ts"],"names":[],"mappings":"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"}
|