@claudexor/budget 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 joi-lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @claudexor/budget
2
+
3
+ Internal package of [Claudexor](https://github.com/razzant/claudexor) — Budget governor: pre-call lease reservation, loop detection, 3-tier circuit breaker, portfolio routing, observed quota.
4
+
5
+ Published as part of the Claudexor toolchain; it follows the monorepo's
6
+ lockstep version and has no separate semver contract. Use the `claudexor`
7
+ CLI (or `@claudexor/cli`) as the supported entry point.
@@ -0,0 +1,5 @@
1
+ export * from "./ledger.js";
2
+ export * from "./router.js";
3
+ export * from "./observe.js";
4
+ export { loadHarnessMetrics, recordHarnessMetric, metricsPath, type HarnessMetric, type HarnessMetrics } from "./metrics.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./ledger.js";
2
+ export * from "./router.js";
3
+ export * from "./observe.js";
4
+ export { loadHarnessMetrics, recordHarnessMetric, metricsPath } from "./metrics.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,WAAW,EAA2C,MAAM,cAAc,CAAC"}
@@ -0,0 +1,82 @@
1
+ import type { BudgetLease, BudgetObservation, Intent } from "@claudexor/schema";
2
+ export type CircuitTier = "ok" | "soft" | "downgrade" | "hard";
3
+ export interface BudgetLimits {
4
+ maxUsd?: number | null;
5
+ }
6
+ export interface CircuitThresholds {
7
+ soft: number;
8
+ downgrade: number;
9
+ hard: number;
10
+ }
11
+ export interface ReserveInput {
12
+ taskId: string;
13
+ attemptId?: string;
14
+ intent: Intent;
15
+ harnessId: string;
16
+ modelHint?: string | null;
17
+ maxUsd?: number | null;
18
+ reason?: string[];
19
+ /**
20
+ * Estimated USD the unit may spend, held against the cap until settled. Holds
21
+ * make concurrent in-flight units visible to `tier()` so a parallel race wave
22
+ * cannot blow past `max_usd` between settlements.
23
+ */
24
+ estimateUsd?: number;
25
+ }
26
+ export interface ReserveResult {
27
+ granted: boolean;
28
+ tier: CircuitTier;
29
+ lease?: BudgetLease;
30
+ reason?: string;
31
+ /** Typed denial cause (no string matching on `reason`): `hard_cap` = the
32
+ * breaker already tripped; `estimate_headroom` = the DD-27 wave guard —
33
+ * this slot's estimate does not fit, but already-granted work continues. */
34
+ denied?: "hard_cap" | "estimate_headroom";
35
+ }
36
+ /** Stable fingerprint of a prompt for loop detection. */
37
+ export declare function promptFingerprint(prompt: string): string;
38
+ /**
39
+ * Dollar-based budget ledger with pre-call reservation, prompt-fingerprint loop
40
+ * detection, and a 3-tier circuit breaker (soft-warn -> downgrade -> hard-kill).
41
+ * Sub-ledgers (children) roll their spend up to the parent.
42
+ */
43
+ export declare class BudgetLedger {
44
+ private readonly limits;
45
+ private readonly thresholds;
46
+ private readonly parent?;
47
+ private readonly leases;
48
+ /** Outstanding USD holds for reserved-but-unsettled leases (amount-bearing). */
49
+ private readonly holds;
50
+ private readonly observations;
51
+ private readonly promptCounts;
52
+ private spendUsd;
53
+ constructor(limits?: BudgetLimits, thresholds?: CircuitThresholds, parent?: BudgetLedger | undefined);
54
+ /** Create a child sub-ledger whose spend rolls up to this one. */
55
+ child(limits?: BudgetLimits): BudgetLedger;
56
+ private outstandingHolds;
57
+ tier(): CircuitTier;
58
+ /** USD still available under the nearest cap in the ledger chain (null = uncapped). */
59
+ private remainingHeadroomUsd;
60
+ reserve(input: ReserveInput): ReserveResult;
61
+ /**
62
+ * Raise a lease's hold to the cost streamed so far (never lowers it). Call from
63
+ * the usage-event stream so `tier()` reflects in-flight spend mid-attempt.
64
+ */
65
+ updateHold(leaseId: string, streamedUsd: number): void;
66
+ private setHold;
67
+ private adjustRollupHold;
68
+ private clearHold;
69
+ settle(leaseId: string, actualUsd: number): void;
70
+ cancel(leaseId: string): void;
71
+ private addRollupSpend;
72
+ spend(): number;
73
+ observe(o: BudgetObservation): void;
74
+ observationsFor(harnessId: string): BudgetObservation[];
75
+ /** True if the harness is in an active cooldown (from an observed rate-limit). */
76
+ cooldownActive(harnessId: string, now?: number): boolean;
77
+ /** Observed remaining headroom (0..1) for a harness, or 1 if unknown. */
78
+ headroom(harnessId: string): number;
79
+ recordPrompt(fingerprint: string): number;
80
+ isLoop(fingerprint: string, threshold?: number): boolean;
81
+ }
82
+ //# sourceMappingURL=ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
package/dist/ledger.js ADDED
@@ -0,0 +1,212 @@
1
+ import { BudgetLease as BudgetLeaseSchema } from "@claudexor/schema";
2
+ import { newId, nowIso, sha256 } from "@claudexor/util";
3
+ /** Stable fingerprint of a prompt for loop detection. */
4
+ export function promptFingerprint(prompt) {
5
+ return sha256(prompt.trim().replace(/\s+/g, " ").toLowerCase());
6
+ }
7
+ /**
8
+ * Dollar-based budget ledger with pre-call reservation, prompt-fingerprint loop
9
+ * detection, and a 3-tier circuit breaker (soft-warn -> downgrade -> hard-kill).
10
+ * Sub-ledgers (children) roll their spend up to the parent.
11
+ */
12
+ export class BudgetLedger {
13
+ limits;
14
+ thresholds;
15
+ parent;
16
+ leases = new Map();
17
+ /** Outstanding USD holds for reserved-but-unsettled leases (amount-bearing). */
18
+ holds = new Map();
19
+ observations = [];
20
+ promptCounts = new Map();
21
+ spendUsd = 0;
22
+ constructor(limits = {}, thresholds = { soft: 0.75, downgrade: 0.9, hard: 1.0 }, parent) {
23
+ this.limits = limits;
24
+ this.thresholds = thresholds;
25
+ this.parent = parent;
26
+ }
27
+ /** Create a child sub-ledger whose spend rolls up to this one. */
28
+ child(limits = {}) {
29
+ return new BudgetLedger(limits, this.thresholds, this);
30
+ }
31
+ outstandingHolds() {
32
+ let sum = 0;
33
+ for (const v of this.holds.values())
34
+ sum += v;
35
+ return sum;
36
+ }
37
+ tier() {
38
+ const cap = this.limits.maxUsd ?? null;
39
+ const localTier = (() => {
40
+ if (cap === null || cap <= 0)
41
+ return "ok";
42
+ // Settled spend + outstanding in-flight holds: a parallel wave of
43
+ // streaming candidates counts against the cap BEFORE settlement.
44
+ const ratio = (this.spendUsd + this.outstandingHolds()) / cap;
45
+ if (ratio >= this.thresholds.hard)
46
+ return "hard";
47
+ if (ratio >= this.thresholds.downgrade)
48
+ return "downgrade";
49
+ if (ratio >= this.thresholds.soft)
50
+ return "soft";
51
+ return "ok";
52
+ })();
53
+ const parentTier = this.parent?.tier() ?? "ok";
54
+ return mostSevere(localTier, parentTier);
55
+ }
56
+ /** USD still available under the nearest cap in the ledger chain (null = uncapped). */
57
+ remainingHeadroomUsd() {
58
+ const cap = this.limits.maxUsd ?? null;
59
+ const local = cap === null || cap <= 0 ? null : cap * this.thresholds.hard - (this.spendUsd + this.outstandingHolds());
60
+ const parent = this.parent?.remainingHeadroomUsd() ?? null;
61
+ if (local === null)
62
+ return parent;
63
+ if (parent === null)
64
+ return local;
65
+ return Math.min(local, parent);
66
+ }
67
+ reserve(input) {
68
+ const tier = this.tier();
69
+ if (tier === "hard") {
70
+ return { granted: false, tier, reason: "budget exhausted (hard cap reached)", denied: "hard_cap" };
71
+ }
72
+ // DD-27 wave guard: an estimate-bearing reservation must FIT UNDER the
73
+ // remaining headroom, or it is DENIED without recording the hold — a
74
+ // denied slot must never poison the tier for candidates that were already
75
+ // granted. `>=` on purpose: an estimate that exactly consumes headroom
76
+ // would trip the hard tier the instant its hold lands, cancelling every
77
+ // in-flight candidate with $0 real spend (the equality case is common —
78
+ // caps and the floor are both round nickels).
79
+ if (typeof input.estimateUsd === "number" && input.estimateUsd > 0) {
80
+ const headroom = this.remainingHeadroomUsd();
81
+ if (headroom !== null && input.estimateUsd >= headroom) {
82
+ return {
83
+ granted: false,
84
+ tier,
85
+ reason: `insufficient headroom for estimated cost (${input.estimateUsd.toFixed(2)} USD >= ${Math.max(headroom, 0).toFixed(2)} USD remaining)`,
86
+ denied: "estimate_headroom",
87
+ };
88
+ }
89
+ }
90
+ const lease = BudgetLeaseSchema.parse({
91
+ lease_id: newId("lease"),
92
+ task_id: input.taskId,
93
+ attempt_id: input.attemptId,
94
+ intent: input.intent,
95
+ harness_id: input.harnessId,
96
+ model_hint: input.modelHint ?? null,
97
+ max_usd: input.maxUsd ?? null,
98
+ reason: input.reason ?? [],
99
+ created_at: nowIso(),
100
+ state: "reserved",
101
+ });
102
+ this.leases.set(lease.lease_id, lease);
103
+ if (typeof input.estimateUsd === "number" && input.estimateUsd > 0) {
104
+ this.setHold(lease.lease_id, input.estimateUsd);
105
+ }
106
+ return { granted: true, tier, lease };
107
+ }
108
+ /**
109
+ * Raise a lease's hold to the cost streamed so far (never lowers it). Call from
110
+ * the usage-event stream so `tier()` reflects in-flight spend mid-attempt.
111
+ */
112
+ updateHold(leaseId, streamedUsd) {
113
+ if (!this.leases.has(leaseId))
114
+ return;
115
+ const current = this.holds.get(leaseId) ?? 0;
116
+ if (streamedUsd > current)
117
+ this.setHold(leaseId, streamedUsd);
118
+ }
119
+ setHold(leaseId, usd) {
120
+ const prev = this.holds.get(leaseId) ?? 0;
121
+ this.holds.set(leaseId, usd);
122
+ this.parent?.adjustRollupHold(leaseId, usd - prev);
123
+ }
124
+ adjustRollupHold(leaseId, deltaUsd) {
125
+ const key = `child:${leaseId}`;
126
+ const prev = this.holds.get(key) ?? 0;
127
+ const next = Math.max(0, prev + deltaUsd);
128
+ if (next === 0)
129
+ this.holds.delete(key);
130
+ else
131
+ this.holds.set(key, next);
132
+ this.parent?.adjustRollupHold(leaseId, deltaUsd);
133
+ }
134
+ clearHold(leaseId) {
135
+ const held = this.holds.get(leaseId) ?? 0;
136
+ this.holds.delete(leaseId);
137
+ if (held > 0)
138
+ this.parent?.adjustRollupHold(leaseId, -held);
139
+ }
140
+ settle(leaseId, actualUsd) {
141
+ const lease = this.leases.get(leaseId);
142
+ // Fail loudly: settling a lease this ledger never granted would attribute
143
+ // spend to nothing (a bookkeeping bug, not a runtime race).
144
+ if (!lease)
145
+ throw new Error(`cannot settle unknown lease: ${leaseId}`);
146
+ // Idempotent for the spend add: a duplicate settle (success path followed
147
+ // by a late error handler) must never double-count real spend. DECIDED
148
+ // TRADEOFF: this no-op can also mask a genuine double-settle bug — spend
149
+ // correctness wins because the duplicate-settle race is a real runtime
150
+ // path (terminal handler + error handler), while a masked bug still
151
+ // surfaces in the rollup totals a test would assert on.
152
+ if (lease.state !== "reserved")
153
+ return;
154
+ lease.state = "settled";
155
+ this.clearHold(leaseId);
156
+ this.spendUsd += actualUsd;
157
+ this.parent?.addRollupSpend(actualUsd);
158
+ }
159
+ cancel(leaseId) {
160
+ const lease = this.leases.get(leaseId);
161
+ if (lease)
162
+ lease.state = "cancelled";
163
+ this.clearHold(leaseId);
164
+ }
165
+ addRollupSpend(usd) {
166
+ this.spendUsd += usd;
167
+ this.parent?.addRollupSpend(usd);
168
+ }
169
+ spend() {
170
+ return this.spendUsd;
171
+ }
172
+ observe(o) {
173
+ this.observations.push(o);
174
+ }
175
+ observationsFor(harnessId) {
176
+ return this.observations.filter((o) => o.harness_id === harnessId);
177
+ }
178
+ /** True if the harness is in an active cooldown (from an observed rate-limit). */
179
+ cooldownActive(harnessId, now = Date.now()) {
180
+ return this.observationsFor(harnessId).some((o) => {
181
+ if (!o.cooldown_until)
182
+ return false;
183
+ const t = Date.parse(o.cooldown_until);
184
+ return Number.isFinite(t) && t > now;
185
+ });
186
+ }
187
+ /** Observed remaining headroom (0..1) for a harness, or 1 if unknown. */
188
+ headroom(harnessId) {
189
+ const used = this.observationsFor(harnessId)
190
+ .map((o) => o.used_percent)
191
+ .filter((v) => typeof v === "number");
192
+ if (used.length === 0)
193
+ return 1;
194
+ const maxUsed = Math.max(...used);
195
+ return Math.max(0, 1 - maxUsed / 100);
196
+ }
197
+ recordPrompt(fingerprint) {
198
+ const n = (this.promptCounts.get(fingerprint) ?? 0) + 1;
199
+ this.promptCounts.set(fingerprint, n);
200
+ return n;
201
+ }
202
+ isLoop(fingerprint, threshold = 3) {
203
+ return (this.promptCounts.get(fingerprint) ?? 0) >= threshold;
204
+ }
205
+ }
206
+ function severity(t) {
207
+ return { ok: 0, soft: 1, downgrade: 2, hard: 3 }[t];
208
+ }
209
+ function mostSevere(a, b) {
210
+ return severity(a) >= severity(b) ? a : b;
211
+ }
212
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.js","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export interface HarnessMetric {
2
+ avg_cost_usd: number | null;
3
+ avg_duration_ms: number | null;
4
+ samples: number;
5
+ }
6
+ export type HarnessMetrics = Record<string, HarnessMetric>;
7
+ export declare function metricsPath(configDir: string): string;
8
+ export declare function loadHarnessMetrics(configDir: string): HarnessMetrics;
9
+ /** Record one settled attempt (EMA update; atomic tmp+rename write). */
10
+ export declare function recordHarnessMetric(configDir: string, harnessId: string, sample: {
11
+ costUsd?: number | null;
12
+ durationMs?: number | null;
13
+ }): void;
14
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAM3D,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAiBpE;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC9D,IAAI,CAyBN"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Harness routing metrics: per-harness rolling averages of OBSERVED
3
+ * attempt cost and duration, persisted under the Claudexor config dir. ONE
4
+ * producer (the orchestrator records after each settled attempt) and ONE
5
+ * consumer (pool ordering fills RouterCandidate.costPerCall/latencyMs).
6
+ * Evidence-based inputs only — never pricing guesses; an attempt with no
7
+ * observed cost records duration alone.
8
+ */
9
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ /** Exponential moving average weight for new samples (recent runs dominate). */
12
+ const EMA_ALPHA = 0.3;
13
+ const MAX_SAMPLES_COUNTED = 1_000_000;
14
+ export function metricsPath(configDir) {
15
+ return join(configDir, "telemetry", "harness-metrics.json");
16
+ }
17
+ export function loadHarnessMetrics(configDir) {
18
+ try {
19
+ const raw = JSON.parse(readFileSync(metricsPath(configDir), "utf8"));
20
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
21
+ return {};
22
+ const out = {};
23
+ for (const [id, v] of Object.entries(raw)) {
24
+ const m = v;
25
+ out[id] = {
26
+ avg_cost_usd: typeof m.avg_cost_usd === "number" && m.avg_cost_usd >= 0 ? m.avg_cost_usd : null,
27
+ avg_duration_ms: typeof m.avg_duration_ms === "number" && m.avg_duration_ms >= 0 ? m.avg_duration_ms : null,
28
+ samples: typeof m.samples === "number" && m.samples > 0 ? Math.min(m.samples, MAX_SAMPLES_COUNTED) : 0,
29
+ };
30
+ }
31
+ return out;
32
+ }
33
+ catch {
34
+ return {};
35
+ }
36
+ }
37
+ /** Record one settled attempt (EMA update; atomic tmp+rename write). */
38
+ export function recordHarnessMetric(configDir, harnessId, sample) {
39
+ const all = loadHarnessMetrics(configDir);
40
+ const prev = all[harnessId] ?? { avg_cost_usd: null, avg_duration_ms: null, samples: 0 };
41
+ const ema = (old, next) => {
42
+ if (typeof next !== "number" || !Number.isFinite(next) || next < 0)
43
+ return old;
44
+ if (old === null)
45
+ return next;
46
+ return old * (1 - EMA_ALPHA) + next * EMA_ALPHA;
47
+ };
48
+ all[harnessId] = {
49
+ avg_cost_usd: ema(prev.avg_cost_usd, sample.costUsd),
50
+ avg_duration_ms: ema(prev.avg_duration_ms, sample.durationMs),
51
+ samples: Math.min(prev.samples + 1, MAX_SAMPLES_COUNTED),
52
+ };
53
+ const path = metricsPath(configDir);
54
+ try {
55
+ mkdirSync(dirname(path), { recursive: true });
56
+ // Unique per WRITE, not per process: two attempts settling concurrently in
57
+ // one daemon collide on a pid-only name (the second rename ENOENTs and
58
+ // silently drops the sample).
59
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
60
+ writeFileSync(tmp, JSON.stringify(all, null, 2) + "\n");
61
+ renameSync(tmp, path);
62
+ }
63
+ catch {
64
+ /* metrics are advisory routing inputs — never fail a run over them */
65
+ }
66
+ }
67
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { BudgetObservation, HarnessEvent } from "@claudexor/schema";
2
+ /**
3
+ * Project an observed budget/quota signal from a TYPED harness event. The budget
4
+ * layer makes NO governance decisions by regex over model/CLI prose: rate-limit
5
+ * detection lives in the adapter parse layer (where native-output translation is
6
+ * legitimate) and arrives here as the typed `HarnessEvent.rate_limit` field.
7
+ * Subscription balancing remains honest "observed best-effort", never exact-claimed.
8
+ */
9
+ /**
10
+ * ONE event can carry SEVERAL budget signals (a codex usage event carries
11
+ * both an estimated cost AND its rollout quota record) — returning a single
12
+ * observation dropped the quota for exactly the real codex shape. Callers
13
+ * observe every entry.
14
+ */
15
+ export declare function observationsFromEvent(harnessId: string, ev: HarnessEvent): BudgetObservation[];
16
+ //# sourceMappingURL=observe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../src/observe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAKzE;;;;;;GAMG;AACH;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,YAAY,GAAG,iBAAiB,EAAE,CAuB9F"}
@@ -0,0 +1,58 @@
1
+ import { nowIso } from "@claudexor/util";
2
+ const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000;
3
+ /**
4
+ * Project an observed budget/quota signal from a TYPED harness event. The budget
5
+ * layer makes NO governance decisions by regex over model/CLI prose: rate-limit
6
+ * detection lives in the adapter parse layer (where native-output translation is
7
+ * legitimate) and arrives here as the typed `HarnessEvent.rate_limit` field.
8
+ * Subscription balancing remains honest "observed best-effort", never exact-claimed.
9
+ */
10
+ /**
11
+ * ONE event can carry SEVERAL budget signals (a codex usage event carries
12
+ * both an estimated cost AND its rollout quota record) — returning a single
13
+ * observation dropped the quota for exactly the real codex shape. Callers
14
+ * observe every entry.
15
+ */
16
+ export function observationsFromEvent(harnessId, ev) {
17
+ const out = [];
18
+ if (ev.type === "usage" && typeof ev.usage?.cost_usd === "number" && ev.usage.cost_usd > 0) {
19
+ // Token-derived costs (e.g. codex) are honest estimates -> "observed"; only
20
+ // natively-reported costs (e.g. claude) are "exact".
21
+ const quality = ev.usage.estimated ? "observed" : "exact";
22
+ out.push({ harness_id: harnessId, ts: nowIso(), quality, kind: "spend", usd: ev.usage.cost_usd });
23
+ }
24
+ if (ev.quota) {
25
+ // The CLI's own machine-readable window record (codex rollout
26
+ // token_count.rate_limits) -> "native" quality used_percent observation.
27
+ out.push({
28
+ harness_id: harnessId,
29
+ ts: nowIso(),
30
+ quality: "native",
31
+ kind: "used_percent",
32
+ used_percent: ev.quota.used_percent,
33
+ resets_at: ev.quota.resets_at ?? null,
34
+ });
35
+ }
36
+ const single = singleObservationFromEvent(harnessId, ev);
37
+ if (single)
38
+ out.push(single);
39
+ return out;
40
+ }
41
+ function singleObservationFromEvent(harnessId, ev) {
42
+ if (ev.rate_limit) {
43
+ const resets = ev.rate_limit.resets_at ?? null;
44
+ const delay = ev.rate_limit.retry_delay_ms ?? null;
45
+ const cooldownUntil = resets ?? new Date(Date.now() + (typeof delay === "number" && delay > 0 ? delay : DEFAULT_COOLDOWN_MS)).toISOString();
46
+ return {
47
+ harness_id: harnessId,
48
+ ts: nowIso(),
49
+ quality: "observed",
50
+ kind: "rate_limited",
51
+ resets_at: resets,
52
+ cooldown_until: cooldownUntil,
53
+ detail: ev.error ?? undefined,
54
+ };
55
+ }
56
+ return null;
57
+ }
58
+ //# sourceMappingURL=observe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observe.js","sourceRoot":"","sources":["../src/observe.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAE1C;;;;;;GAMG;AACH;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,SAAiB,EAAE,EAAgB;IACvE,MAAM,GAAG,GAAwB,EAAE,CAAC;IACpC,IAAI,EAAE,CAAC,IAAI,KAAK,OAAO,IAAI,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,QAAQ,IAAI,EAAE,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;QAC3F,4EAA4E;QAC5E,qDAAqD;QACrD,MAAM,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;QAC1D,GAAG,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;IACpG,CAAC;IACD,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;QACb,8DAA8D;QAC9D,yEAAyE;QACzE,GAAG,CAAC,IAAI,CAAC;YACP,UAAU,EAAE,SAAS;YACrB,EAAE,EAAE,MAAM,EAAE;YACZ,OAAO,EAAE,QAAQ;YACjB,IAAI,EAAE,cAAc;YACpB,YAAY,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY;YACnC,SAAS,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,IAAI,IAAI;SACtC,CAAC,CAAC;IACL,CAAC;IACD,MAAM,MAAM,GAAG,0BAA0B,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACzD,IAAI,MAAM;QAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7B,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,0BAA0B,CAAC,SAAiB,EAAE,EAAgB;IACrE,IAAI,EAAE,CAAC,UAAU,EAAE,CAAC;QAClB,MAAM,MAAM,GAAG,EAAE,CAAC,UAAU,CAAC,SAAS,IAAI,IAAI,CAAC;QAC/C,MAAM,KAAK,GAAG,EAAE,CAAC,UAAU,CAAC,cAAc,IAAI,IAAI,CAAC;QACnD,MAAM,aAAa,GACjB,MAAM,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QACxH,OAAO;YACL,UAAU,EAAE,SAAS;YACrB,EAAE,EAAE,MAAM,EAAE;YACZ,OAAO,EAAE,UAAU;YACnB,IAAI,EAAE,cAAc;YACpB,SAAS,EAAE,MAAM;YACjB,cAAc,EAAE,aAAa;YAC7B,MAAM,EAAE,EAAE,CAAC,KAAK,IAAI,SAAS;SAC9B,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { Portfolio, ProviderFamily } from "@claudexor/schema";
2
+ import type { BudgetLedger } from "./ledger.js";
3
+ export interface RouterCandidate {
4
+ harnessId: string;
5
+ providerFamily: ProviderFamily;
6
+ available: boolean;
7
+ authMode?: "local_session" | "api_key" | "unknown";
8
+ /** 0..1 expected quality for the target intent. */
9
+ qualityForIntent?: number;
10
+ costPerCall?: number;
11
+ latencyMs?: number;
12
+ }
13
+ export interface RouteContext {
14
+ portfolio: Portfolio;
15
+ ledger: BudgetLedger;
16
+ /** Provider families already used (penalized, to encourage cross-family diversity). */
17
+ diversityAgainst?: ProviderFamily[];
18
+ }
19
+ export declare function routeUtility(c: RouterCandidate, ctx: RouteContext): number;
20
+ /** Choose the highest-utility available harness, or null if none are usable. */
21
+ export declare function selectHarness(candidates: RouterCandidate[], ctx: RouteContext): RouterCandidate | null;
22
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,cAAc,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,eAAe,GAAG,SAAS,GAAG,SAAS,CAAC;IACnD,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,cAAc,EAAE,CAAC;CACrC;AA2BD,wBAAgB,YAAY,CAAC,CAAC,EAAE,eAAe,EAAE,GAAG,EAAE,YAAY,GAAG,MAAM,CAkB1E;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,UAAU,EAAE,eAAe,EAAE,EAAE,GAAG,EAAE,YAAY,GAAG,eAAe,GAAG,IAAI,CAWtG"}
package/dist/router.js ADDED
@@ -0,0 +1,48 @@
1
+ function weights(p) {
2
+ switch (p) {
3
+ case "cheapest":
4
+ return { quality: 0.5, cost: 2.0, latency: 1.0, preferSubscription: 1, preferApi: 1 };
5
+ case "strongest":
6
+ return { quality: 2.0, cost: 0.2, latency: 0.5, preferSubscription: 1, preferApi: 1 };
7
+ case "burn":
8
+ return { quality: 2.0, cost: 0.0, latency: 0.2, preferSubscription: 1, preferApi: 1 };
9
+ case "subscription-first":
10
+ return { quality: 1.0, cost: 0.5, latency: 0.5, preferSubscription: 1.6, preferApi: 0.6 };
11
+ case "api-overflow":
12
+ return { quality: 1.0, cost: 0.8, latency: 0.5, preferSubscription: 0.6, preferApi: 1.6 };
13
+ default:
14
+ return { quality: 1.0, cost: 0.7, latency: 0.5, preferSubscription: 1.1, preferApi: 1.0 };
15
+ }
16
+ }
17
+ export function routeUtility(c, ctx) {
18
+ if (!c.available || ctx.ledger.cooldownActive(c.harnessId))
19
+ return 0;
20
+ const w = weights(ctx.portfolio);
21
+ const quality = (c.qualityForIntent ?? 0.5) ** w.quality;
22
+ const headroom = ctx.ledger.headroom(c.harnessId);
23
+ const diversity = ctx.diversityAgainst?.includes(c.providerFamily) ? 0.5 : 1;
24
+ const authPref = c.authMode === "local_session" ? w.preferSubscription : c.authMode === "api_key" ? w.preferApi : 1;
25
+ const conserve = (ctx.portfolio === "conserve-claude" && c.providerFamily === "anthropic") ||
26
+ (ctx.portfolio === "conserve-codex" && c.providerFamily === "openai")
27
+ ? 0.4
28
+ : 1;
29
+ const cost = Math.max(0.0001, c.costPerCall ?? 0.01);
30
+ const latency = Math.max(0.1, (c.latencyMs ?? 1000) / 1000);
31
+ const costFactor = cost ** w.cost;
32
+ const latencyFactor = latency ** w.latency;
33
+ return (quality * headroom * diversity * authPref * conserve) / (costFactor * latencyFactor);
34
+ }
35
+ /** Choose the highest-utility available harness, or null if none are usable. */
36
+ export function selectHarness(candidates, ctx) {
37
+ let best = null;
38
+ let bestU = 0;
39
+ for (const c of candidates) {
40
+ const u = routeUtility(c, ctx);
41
+ if (u > bestU) {
42
+ bestU = u;
43
+ best = c;
44
+ }
45
+ }
46
+ return best;
47
+ }
48
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@claudexor/budget",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "description": "Budget governor: pre-call lease reservation, loop detection, 3-tier circuit breaker, portfolio routing, observed quota.",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@claudexor/schema": "1.0.0",
20
+ "@claudexor/util": "1.0.0"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/razzant/claudexor.git",
25
+ "directory": "packages/budget"
26
+ },
27
+ "homepage": "https://github.com/razzant/claudexor#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/razzant/claudexor/issues"
30
+ },
31
+ "engines": {
32
+ "node": ">=20.19"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "typecheck": "tsc --noEmit"
40
+ }
41
+ }