floe-guard 0.2.0 → 0.3.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/README.md +21 -0
- package/dist/index.cjs +206 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -4
- package/dist/index.d.ts +78 -4
- package/dist/index.js +206 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cost_map.json +60 -0
package/dist/index.d.ts
CHANGED
|
@@ -23,8 +23,9 @@ interface PricedModel {
|
|
|
23
23
|
/**
|
|
24
24
|
* Resolve a model to its per-token price, or `null` if it cannot be priced.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Per specificity group (exact forms first, date-stripped fallbacks second):
|
|
27
|
+
* overrides win, then the bundled cost map. Fail-closed: the first matching
|
|
28
|
+
* entry must have finite prices, else `null`.
|
|
28
29
|
*/
|
|
29
30
|
declare function resolvePrice(model: string, overrides?: Record<string, ManualPrice>): PricedModel | null;
|
|
30
31
|
/** USD cost for token usage. Negative counts are clamped to zero. */
|
|
@@ -63,6 +64,32 @@ declare namespace pricing {
|
|
|
63
64
|
* same epsilon handling, same fail-closed default.
|
|
64
65
|
*/
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* One priced spend event in the guard's per-call ledger.
|
|
69
|
+
*
|
|
70
|
+
* Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /
|
|
71
|
+
* {@link BudgetGuard.recordTool} that accrues spend appends exactly one event, so
|
|
72
|
+
* the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has
|
|
73
|
+
* evicted old events). The schema is identical in the Python
|
|
74
|
+
* package (`SpendEvent` in `src/floe_guard/guard.py`) and
|
|
75
|
+
* {@link BudgetGuard.exportLog} serialises it with the same snake_case keys in
|
|
76
|
+
* both languages, so every agent emits the same shape regardless of stack.
|
|
77
|
+
*/
|
|
78
|
+
interface SpendEvent {
|
|
79
|
+
/** Unix epoch seconds (UTC). */
|
|
80
|
+
readonly timestamp: number;
|
|
81
|
+
readonly kind: "llm" | "tool";
|
|
82
|
+
readonly modelOrTool: string;
|
|
83
|
+
/** `null` for tool events. */
|
|
84
|
+
readonly promptTokens: number | null;
|
|
85
|
+
/** `null` for tool events. */
|
|
86
|
+
readonly completionTokens: number | null;
|
|
87
|
+
readonly costUsd: number;
|
|
88
|
+
/** Caller-supplied tag (agent/task name). */
|
|
89
|
+
readonly label?: string;
|
|
90
|
+
/** The reservation settled by this call, if any. */
|
|
91
|
+
readonly reserved?: number;
|
|
92
|
+
}
|
|
66
93
|
interface BudgetGuardOptions {
|
|
67
94
|
/** Per-model manual prices for models the bundled cost map cannot price. */
|
|
68
95
|
priceOverrides?: Record<string, ManualPrice>;
|
|
@@ -83,6 +110,13 @@ interface BudgetGuardOptions {
|
|
|
83
110
|
* flags `nearLimit` so an agent can taper before the hard-stop. Default 8000.
|
|
84
111
|
*/
|
|
85
112
|
nearLimitBps?: number;
|
|
113
|
+
/**
|
|
114
|
+
* Optional cap on the per-call spend ledger ({@link BudgetGuard.spendLog}).
|
|
115
|
+
* When set, the ledger is a ring buffer keeping the most recent N events so a
|
|
116
|
+
* long-running agent's memory stays bounded; the running totals are
|
|
117
|
+
* unaffected. Default: keep every event.
|
|
118
|
+
*/
|
|
119
|
+
maxLogEvents?: number;
|
|
86
120
|
}
|
|
87
121
|
/**
|
|
88
122
|
* A context-aware spend signal for the single local budget.
|
|
@@ -119,6 +153,9 @@ declare class BudgetGuard {
|
|
|
119
153
|
private lastCost;
|
|
120
154
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
121
155
|
private reserved;
|
|
156
|
+
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
157
|
+
private readonly spendEvents;
|
|
158
|
+
private readonly maxLogEvents?;
|
|
122
159
|
/**
|
|
123
160
|
* @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
|
|
124
161
|
*/
|
|
@@ -151,11 +188,15 @@ declare class BudgetGuard {
|
|
|
151
188
|
* Release a reservation and record the actual cost. `record` is `settle` with
|
|
152
189
|
* no reservation. Returns the USD cost of this call; unpriceable-model handling
|
|
153
190
|
* matches {@link BudgetGuard.record}, and any held reservation is released even
|
|
154
|
-
* on the warn-and-skip path.
|
|
191
|
+
* on the warn-and-skip path. A priced call appends one {@link SpendEvent} to
|
|
192
|
+
* {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);
|
|
193
|
+
* the warn-and-skip path accrues nothing and logs nothing, so the ledger stays
|
|
194
|
+
* in lockstep with `spentUsd`.
|
|
155
195
|
*/
|
|
156
196
|
settle(model: string, promptTokens: number, completionTokens: number, options?: {
|
|
157
197
|
reserved?: number;
|
|
158
198
|
price?: ManualPrice;
|
|
199
|
+
label?: string;
|
|
159
200
|
}): number;
|
|
160
201
|
/**
|
|
161
202
|
* Price one response's tokens offline and add the cost to the total.
|
|
@@ -166,6 +207,21 @@ declare class BudgetGuard {
|
|
|
166
207
|
*/
|
|
167
208
|
record(model: string, promptTokens: number, completionTokens: number, options?: {
|
|
168
209
|
price?: ManualPrice;
|
|
210
|
+
label?: string;
|
|
211
|
+
}): number;
|
|
212
|
+
/**
|
|
213
|
+
* Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
|
|
214
|
+
*
|
|
215
|
+
* Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
|
|
216
|
+
* same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
|
|
217
|
+
* `check()` / `reserve()` see them) and appends a `kind: "tool"`
|
|
218
|
+
* {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
|
|
219
|
+
* cost: tools have no token usage to price. Deliberately does NOT update the
|
|
220
|
+
* next-call estimate — that predicts the next *LLM* call, and a tool's price
|
|
221
|
+
* would skew it. Returns `costUsd`.
|
|
222
|
+
*/
|
|
223
|
+
recordTool(tool: string, costUsd: number, options?: {
|
|
224
|
+
label?: string;
|
|
169
225
|
}): number;
|
|
170
226
|
/**
|
|
171
227
|
* Drop an in-flight reservation without recording spend (e.g. the call failed
|
|
@@ -174,6 +230,24 @@ declare class BudgetGuard {
|
|
|
174
230
|
release(reserved: number): void;
|
|
175
231
|
/** USD left before the ceiling, net of in-flight reservations (never negative). */
|
|
176
232
|
get remainingUsd(): number;
|
|
233
|
+
/**
|
|
234
|
+
* The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
|
|
235
|
+
* `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
|
|
236
|
+
* it cannot corrupt the ledger.
|
|
237
|
+
*/
|
|
238
|
+
get spendLog(): SpendEvent[];
|
|
239
|
+
/**
|
|
240
|
+
* The spend ledger as JSONL — one event per line, newline-terminated.
|
|
241
|
+
*
|
|
242
|
+
* The schema is stable and language-independent (snake_case keys, fixed order;
|
|
243
|
+
* optional fields omitted when absent), identical to the Python package's
|
|
244
|
+
* `export_log()`, so heterogeneous agents produce logs you can concatenate and
|
|
245
|
+
* analyse as one stream. (The *schema* is the contract, not the bytes: the two
|
|
246
|
+
* runtimes may render the same float differently, e.g. JS `0.0000025` vs
|
|
247
|
+
* Python `2.5e-06`.) Empty ledger yields `""`.
|
|
248
|
+
*/
|
|
249
|
+
exportLog(): string;
|
|
250
|
+
private appendEvent;
|
|
177
251
|
/**
|
|
178
252
|
* Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
|
|
179
253
|
*
|
|
@@ -289,4 +363,4 @@ interface BudgetGuardMiddleware {
|
|
|
289
363
|
*/
|
|
290
364
|
declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
|
|
291
365
|
|
|
292
|
-
export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
|
|
366
|
+
export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
|
package/dist/index.js
CHANGED
|
@@ -182,6 +182,12 @@ var cost_map_default = {
|
|
|
182
182
|
litellm_provider: "anthropic",
|
|
183
183
|
mode: "chat"
|
|
184
184
|
},
|
|
185
|
+
"claude-sonnet-5": {
|
|
186
|
+
input_cost_per_token: 2e-6,
|
|
187
|
+
output_cost_per_token: 1e-5,
|
|
188
|
+
litellm_provider: "anthropic",
|
|
189
|
+
mode: "chat"
|
|
190
|
+
},
|
|
185
191
|
"ft:gpt-3.5-turbo": {
|
|
186
192
|
input_cost_per_token: 3e-6,
|
|
187
193
|
output_cost_per_token: 6e-6,
|
|
@@ -632,6 +638,30 @@ var cost_map_default = {
|
|
|
632
638
|
litellm_provider: "openai",
|
|
633
639
|
mode: "chat"
|
|
634
640
|
},
|
|
641
|
+
"gpt-5.6": {
|
|
642
|
+
input_cost_per_token: 5e-6,
|
|
643
|
+
output_cost_per_token: 3e-5,
|
|
644
|
+
litellm_provider: "openai",
|
|
645
|
+
mode: "chat"
|
|
646
|
+
},
|
|
647
|
+
"gpt-5.6-luna": {
|
|
648
|
+
input_cost_per_token: 1e-6,
|
|
649
|
+
output_cost_per_token: 6e-6,
|
|
650
|
+
litellm_provider: "openai",
|
|
651
|
+
mode: "chat"
|
|
652
|
+
},
|
|
653
|
+
"gpt-5.6-sol": {
|
|
654
|
+
input_cost_per_token: 5e-6,
|
|
655
|
+
output_cost_per_token: 3e-5,
|
|
656
|
+
litellm_provider: "openai",
|
|
657
|
+
mode: "chat"
|
|
658
|
+
},
|
|
659
|
+
"gpt-5.6-terra": {
|
|
660
|
+
input_cost_per_token: 25e-7,
|
|
661
|
+
output_cost_per_token: 15e-6,
|
|
662
|
+
litellm_provider: "openai",
|
|
663
|
+
mode: "chat"
|
|
664
|
+
},
|
|
635
665
|
"gpt-audio": {
|
|
636
666
|
input_cost_per_token: 25e-7,
|
|
637
667
|
output_cost_per_token: 1e-5,
|
|
@@ -686,6 +716,18 @@ var cost_map_default = {
|
|
|
686
716
|
litellm_provider: "openai",
|
|
687
717
|
mode: "chat"
|
|
688
718
|
},
|
|
719
|
+
"gpt-realtime-2.1": {
|
|
720
|
+
input_cost_per_token: 4e-6,
|
|
721
|
+
output_cost_per_token: 24e-6,
|
|
722
|
+
litellm_provider: "openai",
|
|
723
|
+
mode: "chat"
|
|
724
|
+
},
|
|
725
|
+
"gpt-realtime-2.1-mini": {
|
|
726
|
+
input_cost_per_token: 6e-7,
|
|
727
|
+
output_cost_per_token: 24e-7,
|
|
728
|
+
litellm_provider: "openai",
|
|
729
|
+
mode: "chat"
|
|
730
|
+
},
|
|
689
731
|
"gpt-realtime-2025-08-28": {
|
|
690
732
|
input_cost_per_token: 4e-6,
|
|
691
733
|
output_cost_per_token: 16e-6,
|
|
@@ -776,6 +818,24 @@ var cost_map_default = {
|
|
|
776
818
|
litellm_provider: "openai",
|
|
777
819
|
mode: "chat"
|
|
778
820
|
},
|
|
821
|
+
"openai/gpt-oss-120b": {
|
|
822
|
+
input_cost_per_token: 15e-8,
|
|
823
|
+
output_cost_per_token: 6e-7,
|
|
824
|
+
litellm_provider: "groq",
|
|
825
|
+
mode: "chat"
|
|
826
|
+
},
|
|
827
|
+
"openai/gpt-oss-20b": {
|
|
828
|
+
input_cost_per_token: 75e-9,
|
|
829
|
+
output_cost_per_token: 3e-7,
|
|
830
|
+
litellm_provider: "groq",
|
|
831
|
+
mode: "chat"
|
|
832
|
+
},
|
|
833
|
+
"openai/gpt-oss-safeguard-20b": {
|
|
834
|
+
input_cost_per_token: 75e-9,
|
|
835
|
+
output_cost_per_token: 3e-7,
|
|
836
|
+
litellm_provider: "groq",
|
|
837
|
+
mode: "chat"
|
|
838
|
+
},
|
|
779
839
|
"qwen/qwen3-32b": {
|
|
780
840
|
input_cost_per_token: 29e-8,
|
|
781
841
|
output_cost_per_token: 59e-8,
|
|
@@ -810,39 +870,64 @@ var cost_map_default = {
|
|
|
810
870
|
|
|
811
871
|
// src/pricing.ts
|
|
812
872
|
var COST_MAP = cost_map_default;
|
|
813
|
-
|
|
873
|
+
var PROVIDER_PREFIXES = /* @__PURE__ */ new Set(["groq"]);
|
|
874
|
+
var DATE_SUFFIX = /-(?:\d{8}|\d{4}-\d{2}-\d{2})$/;
|
|
875
|
+
function candidateGroups(model) {
|
|
814
876
|
const m = model.trim();
|
|
815
|
-
const
|
|
816
|
-
|
|
877
|
+
const base = [m];
|
|
878
|
+
const firstSlash = m.indexOf("/");
|
|
879
|
+
if (firstSlash !== -1 && PROVIDER_PREFIXES.has(m.slice(0, firstSlash))) {
|
|
880
|
+
base.push(m.slice(firstSlash + 1));
|
|
881
|
+
}
|
|
882
|
+
const lastSlash = m.lastIndexOf("/");
|
|
883
|
+
if (lastSlash !== -1) {
|
|
884
|
+
base.push(m.slice(lastSlash + 1));
|
|
885
|
+
}
|
|
886
|
+
const exact = [];
|
|
887
|
+
for (const cand of base) {
|
|
888
|
+
if (cand && !exact.includes(cand)) exact.push(cand);
|
|
889
|
+
}
|
|
890
|
+
const stripped = [];
|
|
891
|
+
for (const cand of exact) {
|
|
892
|
+
const c = cand.replace(DATE_SUFFIX, "");
|
|
893
|
+
if (c && !exact.includes(c) && !stripped.includes(c)) stripped.push(c);
|
|
894
|
+
}
|
|
895
|
+
return [exact, stripped];
|
|
817
896
|
}
|
|
818
897
|
function bothFinite(a, b) {
|
|
819
898
|
return typeof a === "number" && typeof b === "number" && Number.isFinite(a) && Number.isFinite(b);
|
|
820
899
|
}
|
|
821
900
|
function resolvePrice(model, overrides) {
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
901
|
+
for (const cands of candidateGroups(model)) {
|
|
902
|
+
if (overrides) {
|
|
903
|
+
for (const cand of cands) {
|
|
904
|
+
const ov = Object.prototype.hasOwnProperty.call(overrides, cand) ? overrides[cand] : void 0;
|
|
905
|
+
if (ov !== void 0) {
|
|
906
|
+
if (bothFinite(ov.inputCostPerToken, ov.outputCostPerToken)) {
|
|
907
|
+
return {
|
|
908
|
+
inputCostPerToken: ov.inputCostPerToken,
|
|
909
|
+
outputCostPerToken: ov.outputCostPerToken,
|
|
910
|
+
source: "override"
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
832
915
|
}
|
|
833
|
-
|
|
916
|
+
}
|
|
917
|
+
for (const cand of cands) {
|
|
918
|
+
const entry = Object.prototype.hasOwnProperty.call(COST_MAP, cand) ? COST_MAP[cand] : void 0;
|
|
919
|
+
if (!entry) continue;
|
|
920
|
+
const input = entry.input_cost_per_token;
|
|
921
|
+
const output = entry.output_cost_per_token;
|
|
922
|
+
if (!bothFinite(input, output)) return null;
|
|
923
|
+
return {
|
|
924
|
+
inputCostPerToken: input,
|
|
925
|
+
outputCostPerToken: output,
|
|
926
|
+
source: "cost_map"
|
|
927
|
+
};
|
|
834
928
|
}
|
|
835
929
|
}
|
|
836
|
-
|
|
837
|
-
if (!entry) return null;
|
|
838
|
-
const input = entry.input_cost_per_token;
|
|
839
|
-
const output = entry.output_cost_per_token;
|
|
840
|
-
if (!bothFinite(input, output)) return null;
|
|
841
|
-
return {
|
|
842
|
-
inputCostPerToken: input,
|
|
843
|
-
outputCostPerToken: output,
|
|
844
|
-
source: "cost_map"
|
|
845
|
-
};
|
|
930
|
+
return null;
|
|
846
931
|
}
|
|
847
932
|
function priceTokens(priced, promptTokens, completionTokens) {
|
|
848
933
|
const p = Math.max(0, promptTokens);
|
|
@@ -867,6 +952,9 @@ var BudgetGuard = class {
|
|
|
867
952
|
lastCost = 0;
|
|
868
953
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
869
954
|
reserved = 0;
|
|
955
|
+
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
956
|
+
spendEvents = [];
|
|
957
|
+
maxLogEvents;
|
|
870
958
|
/**
|
|
871
959
|
* @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
|
|
872
960
|
*/
|
|
@@ -882,7 +970,13 @@ var BudgetGuard = class {
|
|
|
882
970
|
`nearLimitBps must be an integer in 0..10000, got ${nearLimitBps}`
|
|
883
971
|
);
|
|
884
972
|
}
|
|
973
|
+
if (options.maxLogEvents !== void 0 && (!Number.isInteger(options.maxLogEvents) || options.maxLogEvents < 0)) {
|
|
974
|
+
throw new RangeError(
|
|
975
|
+
`maxLogEvents must be a non-negative integer, got ${options.maxLogEvents}`
|
|
976
|
+
);
|
|
977
|
+
}
|
|
885
978
|
this.limitUsd = limitUsd;
|
|
979
|
+
this.maxLogEvents = options.maxLogEvents;
|
|
886
980
|
this.priceOverrides = options.priceOverrides;
|
|
887
981
|
this.failClosed = options.failClosed ?? true;
|
|
888
982
|
this.onBlock = options.onBlock ?? defaultOnBlock;
|
|
@@ -944,7 +1038,10 @@ var BudgetGuard = class {
|
|
|
944
1038
|
* Release a reservation and record the actual cost. `record` is `settle` with
|
|
945
1039
|
* no reservation. Returns the USD cost of this call; unpriceable-model handling
|
|
946
1040
|
* matches {@link BudgetGuard.record}, and any held reservation is released even
|
|
947
|
-
* on the warn-and-skip path.
|
|
1041
|
+
* on the warn-and-skip path. A priced call appends one {@link SpendEvent} to
|
|
1042
|
+
* {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);
|
|
1043
|
+
* the warn-and-skip path accrues nothing and logs nothing, so the ledger stays
|
|
1044
|
+
* in lockstep with `spentUsd`.
|
|
948
1045
|
*/
|
|
949
1046
|
settle(model, promptTokens, completionTokens, options = {}) {
|
|
950
1047
|
const reserved = options.reserved ?? 0;
|
|
@@ -981,6 +1078,18 @@ var BudgetGuard = class {
|
|
|
981
1078
|
this.spentUsd = this.limitUsd;
|
|
982
1079
|
}
|
|
983
1080
|
this.lastCost = cost;
|
|
1081
|
+
this.appendEvent({
|
|
1082
|
+
timestamp: Date.now() / 1e3,
|
|
1083
|
+
kind: "llm",
|
|
1084
|
+
modelOrTool: model,
|
|
1085
|
+
promptTokens,
|
|
1086
|
+
completionTokens,
|
|
1087
|
+
costUsd: cost,
|
|
1088
|
+
...options.label !== void 0 ? { label: options.label } : {},
|
|
1089
|
+
// 0 means "no reservation" (the plain record() path) — omit rather than
|
|
1090
|
+
// log a meaningless zero.
|
|
1091
|
+
...reserved ? { reserved } : {}
|
|
1092
|
+
});
|
|
984
1093
|
return cost;
|
|
985
1094
|
}
|
|
986
1095
|
/**
|
|
@@ -993,9 +1102,40 @@ var BudgetGuard = class {
|
|
|
993
1102
|
record(model, promptTokens, completionTokens, options = {}) {
|
|
994
1103
|
return this.settle(model, promptTokens, completionTokens, {
|
|
995
1104
|
reserved: 0,
|
|
996
|
-
price: options.price
|
|
1105
|
+
price: options.price,
|
|
1106
|
+
label: options.label
|
|
997
1107
|
});
|
|
998
1108
|
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
|
|
1111
|
+
*
|
|
1112
|
+
* Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
|
|
1113
|
+
* same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
|
|
1114
|
+
* `check()` / `reserve()` see them) and appends a `kind: "tool"`
|
|
1115
|
+
* {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
|
|
1116
|
+
* cost: tools have no token usage to price. Deliberately does NOT update the
|
|
1117
|
+
* next-call estimate — that predicts the next *LLM* call, and a tool's price
|
|
1118
|
+
* would skew it. Returns `costUsd`.
|
|
1119
|
+
*/
|
|
1120
|
+
recordTool(tool, costUsd, options = {}) {
|
|
1121
|
+
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
1122
|
+
throw new RangeError(`costUsd must be a finite, non-negative number, got ${costUsd}`);
|
|
1123
|
+
}
|
|
1124
|
+
this.spentUsd += costUsd;
|
|
1125
|
+
if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
|
|
1126
|
+
this.spentUsd = this.limitUsd;
|
|
1127
|
+
}
|
|
1128
|
+
this.appendEvent({
|
|
1129
|
+
timestamp: Date.now() / 1e3,
|
|
1130
|
+
kind: "tool",
|
|
1131
|
+
modelOrTool: tool,
|
|
1132
|
+
promptTokens: null,
|
|
1133
|
+
completionTokens: null,
|
|
1134
|
+
costUsd,
|
|
1135
|
+
...options.label !== void 0 ? { label: options.label } : {}
|
|
1136
|
+
});
|
|
1137
|
+
return costUsd;
|
|
1138
|
+
}
|
|
999
1139
|
/**
|
|
1000
1140
|
* Drop an in-flight reservation without recording spend (e.g. the call failed
|
|
1001
1141
|
* before producing usage). Safe to call with `0`.
|
|
@@ -1011,6 +1151,46 @@ var BudgetGuard = class {
|
|
|
1011
1151
|
get remainingUsd() {
|
|
1012
1152
|
return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);
|
|
1013
1153
|
}
|
|
1154
|
+
/**
|
|
1155
|
+
* The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
|
|
1156
|
+
* `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
|
|
1157
|
+
* it cannot corrupt the ledger.
|
|
1158
|
+
*/
|
|
1159
|
+
get spendLog() {
|
|
1160
|
+
return [...this.spendEvents];
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* The spend ledger as JSONL — one event per line, newline-terminated.
|
|
1164
|
+
*
|
|
1165
|
+
* The schema is stable and language-independent (snake_case keys, fixed order;
|
|
1166
|
+
* optional fields omitted when absent), identical to the Python package's
|
|
1167
|
+
* `export_log()`, so heterogeneous agents produce logs you can concatenate and
|
|
1168
|
+
* analyse as one stream. (The *schema* is the contract, not the bytes: the two
|
|
1169
|
+
* runtimes may render the same float differently, e.g. JS `0.0000025` vs
|
|
1170
|
+
* Python `2.5e-06`.) Empty ledger yields `""`.
|
|
1171
|
+
*/
|
|
1172
|
+
exportLog() {
|
|
1173
|
+
return this.spendEvents.map((e) => {
|
|
1174
|
+
const row = {
|
|
1175
|
+
timestamp: e.timestamp,
|
|
1176
|
+
kind: e.kind,
|
|
1177
|
+
model_or_tool: e.modelOrTool,
|
|
1178
|
+
prompt_tokens: e.promptTokens,
|
|
1179
|
+
completion_tokens: e.completionTokens,
|
|
1180
|
+
cost_usd: e.costUsd
|
|
1181
|
+
};
|
|
1182
|
+
if (e.label !== void 0) row.label = e.label;
|
|
1183
|
+
if (e.reserved !== void 0) row.reserved = e.reserved;
|
|
1184
|
+
return `${JSON.stringify(row)}
|
|
1185
|
+
`;
|
|
1186
|
+
}).join("");
|
|
1187
|
+
}
|
|
1188
|
+
appendEvent(event) {
|
|
1189
|
+
this.spendEvents.push(Object.freeze(event));
|
|
1190
|
+
if (this.maxLogEvents !== void 0 && this.spendEvents.length > this.maxLogEvents) {
|
|
1191
|
+
this.spendEvents.splice(0, this.spendEvents.length - this.maxLogEvents);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1014
1194
|
/**
|
|
1015
1195
|
* Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
|
|
1016
1196
|
*
|