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/README.md
CHANGED
|
@@ -63,6 +63,27 @@ const adv = guard.advisory();
|
|
|
63
63
|
const model = adv.nearLimit ? openai("gpt-4o-mini") : openai("gpt-4o");
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## Per-call spend log
|
|
67
|
+
|
|
68
|
+
The guard keeps a typed, in-memory ledger of everything it priced: each
|
|
69
|
+
`record()` / `settle()` appends one `SpendEvent`, and `recordTool()` lets paid
|
|
70
|
+
non-LLM calls spend the same budget and land in the same log. The events sum to
|
|
71
|
+
`spentUsd` (unless a `maxLogEvents` ring buffer has evicted old ones).
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const guard = new BudgetGuard(1.0); // { maxLogEvents: N } caps memory
|
|
75
|
+
guard.record("gpt-4o", 1_200, 350, { label: "researcher" });
|
|
76
|
+
guard.recordTool("serpapi.search", 0.01, { label: "researcher" });
|
|
77
|
+
|
|
78
|
+
guard.spendLog; // [{ timestamp, kind: "llm", modelOrTool: "gpt-4o", … }, …]
|
|
79
|
+
process.stdout.write(guard.exportLog()); // JSONL, one event per line
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`exportLog()` emits a stable snake_case schema —
|
|
83
|
+
`{timestamp, kind: llm|tool, model_or_tool, prompt_tokens, completion_tokens,
|
|
84
|
+
cost_usd, label?, reserved?}` — identical to the Python package's
|
|
85
|
+
`export_log()`, so every agent produces the same shape regardless of stack.
|
|
86
|
+
|
|
66
87
|
## Compatibility
|
|
67
88
|
|
|
68
89
|
`ai` is declared as a peer dependency with the range `>=4.0.0 <6.0.0`:
|
package/dist/index.cjs
CHANGED
|
@@ -209,6 +209,12 @@ var cost_map_default = {
|
|
|
209
209
|
litellm_provider: "anthropic",
|
|
210
210
|
mode: "chat"
|
|
211
211
|
},
|
|
212
|
+
"claude-sonnet-5": {
|
|
213
|
+
input_cost_per_token: 2e-6,
|
|
214
|
+
output_cost_per_token: 1e-5,
|
|
215
|
+
litellm_provider: "anthropic",
|
|
216
|
+
mode: "chat"
|
|
217
|
+
},
|
|
212
218
|
"ft:gpt-3.5-turbo": {
|
|
213
219
|
input_cost_per_token: 3e-6,
|
|
214
220
|
output_cost_per_token: 6e-6,
|
|
@@ -659,6 +665,30 @@ var cost_map_default = {
|
|
|
659
665
|
litellm_provider: "openai",
|
|
660
666
|
mode: "chat"
|
|
661
667
|
},
|
|
668
|
+
"gpt-5.6": {
|
|
669
|
+
input_cost_per_token: 5e-6,
|
|
670
|
+
output_cost_per_token: 3e-5,
|
|
671
|
+
litellm_provider: "openai",
|
|
672
|
+
mode: "chat"
|
|
673
|
+
},
|
|
674
|
+
"gpt-5.6-luna": {
|
|
675
|
+
input_cost_per_token: 1e-6,
|
|
676
|
+
output_cost_per_token: 6e-6,
|
|
677
|
+
litellm_provider: "openai",
|
|
678
|
+
mode: "chat"
|
|
679
|
+
},
|
|
680
|
+
"gpt-5.6-sol": {
|
|
681
|
+
input_cost_per_token: 5e-6,
|
|
682
|
+
output_cost_per_token: 3e-5,
|
|
683
|
+
litellm_provider: "openai",
|
|
684
|
+
mode: "chat"
|
|
685
|
+
},
|
|
686
|
+
"gpt-5.6-terra": {
|
|
687
|
+
input_cost_per_token: 25e-7,
|
|
688
|
+
output_cost_per_token: 15e-6,
|
|
689
|
+
litellm_provider: "openai",
|
|
690
|
+
mode: "chat"
|
|
691
|
+
},
|
|
662
692
|
"gpt-audio": {
|
|
663
693
|
input_cost_per_token: 25e-7,
|
|
664
694
|
output_cost_per_token: 1e-5,
|
|
@@ -713,6 +743,18 @@ var cost_map_default = {
|
|
|
713
743
|
litellm_provider: "openai",
|
|
714
744
|
mode: "chat"
|
|
715
745
|
},
|
|
746
|
+
"gpt-realtime-2.1": {
|
|
747
|
+
input_cost_per_token: 4e-6,
|
|
748
|
+
output_cost_per_token: 24e-6,
|
|
749
|
+
litellm_provider: "openai",
|
|
750
|
+
mode: "chat"
|
|
751
|
+
},
|
|
752
|
+
"gpt-realtime-2.1-mini": {
|
|
753
|
+
input_cost_per_token: 6e-7,
|
|
754
|
+
output_cost_per_token: 24e-7,
|
|
755
|
+
litellm_provider: "openai",
|
|
756
|
+
mode: "chat"
|
|
757
|
+
},
|
|
716
758
|
"gpt-realtime-2025-08-28": {
|
|
717
759
|
input_cost_per_token: 4e-6,
|
|
718
760
|
output_cost_per_token: 16e-6,
|
|
@@ -803,6 +845,24 @@ var cost_map_default = {
|
|
|
803
845
|
litellm_provider: "openai",
|
|
804
846
|
mode: "chat"
|
|
805
847
|
},
|
|
848
|
+
"openai/gpt-oss-120b": {
|
|
849
|
+
input_cost_per_token: 15e-8,
|
|
850
|
+
output_cost_per_token: 6e-7,
|
|
851
|
+
litellm_provider: "groq",
|
|
852
|
+
mode: "chat"
|
|
853
|
+
},
|
|
854
|
+
"openai/gpt-oss-20b": {
|
|
855
|
+
input_cost_per_token: 75e-9,
|
|
856
|
+
output_cost_per_token: 3e-7,
|
|
857
|
+
litellm_provider: "groq",
|
|
858
|
+
mode: "chat"
|
|
859
|
+
},
|
|
860
|
+
"openai/gpt-oss-safeguard-20b": {
|
|
861
|
+
input_cost_per_token: 75e-9,
|
|
862
|
+
output_cost_per_token: 3e-7,
|
|
863
|
+
litellm_provider: "groq",
|
|
864
|
+
mode: "chat"
|
|
865
|
+
},
|
|
806
866
|
"qwen/qwen3-32b": {
|
|
807
867
|
input_cost_per_token: 29e-8,
|
|
808
868
|
output_cost_per_token: 59e-8,
|
|
@@ -837,39 +897,64 @@ var cost_map_default = {
|
|
|
837
897
|
|
|
838
898
|
// src/pricing.ts
|
|
839
899
|
var COST_MAP = cost_map_default;
|
|
840
|
-
|
|
900
|
+
var PROVIDER_PREFIXES = /* @__PURE__ */ new Set(["groq"]);
|
|
901
|
+
var DATE_SUFFIX = /-(?:\d{8}|\d{4}-\d{2}-\d{2})$/;
|
|
902
|
+
function candidateGroups(model) {
|
|
841
903
|
const m = model.trim();
|
|
842
|
-
const
|
|
843
|
-
|
|
904
|
+
const base = [m];
|
|
905
|
+
const firstSlash = m.indexOf("/");
|
|
906
|
+
if (firstSlash !== -1 && PROVIDER_PREFIXES.has(m.slice(0, firstSlash))) {
|
|
907
|
+
base.push(m.slice(firstSlash + 1));
|
|
908
|
+
}
|
|
909
|
+
const lastSlash = m.lastIndexOf("/");
|
|
910
|
+
if (lastSlash !== -1) {
|
|
911
|
+
base.push(m.slice(lastSlash + 1));
|
|
912
|
+
}
|
|
913
|
+
const exact = [];
|
|
914
|
+
for (const cand of base) {
|
|
915
|
+
if (cand && !exact.includes(cand)) exact.push(cand);
|
|
916
|
+
}
|
|
917
|
+
const stripped = [];
|
|
918
|
+
for (const cand of exact) {
|
|
919
|
+
const c = cand.replace(DATE_SUFFIX, "");
|
|
920
|
+
if (c && !exact.includes(c) && !stripped.includes(c)) stripped.push(c);
|
|
921
|
+
}
|
|
922
|
+
return [exact, stripped];
|
|
844
923
|
}
|
|
845
924
|
function bothFinite(a, b) {
|
|
846
925
|
return typeof a === "number" && typeof b === "number" && Number.isFinite(a) && Number.isFinite(b);
|
|
847
926
|
}
|
|
848
927
|
function resolvePrice(model, overrides) {
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
928
|
+
for (const cands of candidateGroups(model)) {
|
|
929
|
+
if (overrides) {
|
|
930
|
+
for (const cand of cands) {
|
|
931
|
+
const ov = Object.prototype.hasOwnProperty.call(overrides, cand) ? overrides[cand] : void 0;
|
|
932
|
+
if (ov !== void 0) {
|
|
933
|
+
if (bothFinite(ov.inputCostPerToken, ov.outputCostPerToken)) {
|
|
934
|
+
return {
|
|
935
|
+
inputCostPerToken: ov.inputCostPerToken,
|
|
936
|
+
outputCostPerToken: ov.outputCostPerToken,
|
|
937
|
+
source: "override"
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
859
942
|
}
|
|
860
|
-
|
|
943
|
+
}
|
|
944
|
+
for (const cand of cands) {
|
|
945
|
+
const entry = Object.prototype.hasOwnProperty.call(COST_MAP, cand) ? COST_MAP[cand] : void 0;
|
|
946
|
+
if (!entry) continue;
|
|
947
|
+
const input = entry.input_cost_per_token;
|
|
948
|
+
const output = entry.output_cost_per_token;
|
|
949
|
+
if (!bothFinite(input, output)) return null;
|
|
950
|
+
return {
|
|
951
|
+
inputCostPerToken: input,
|
|
952
|
+
outputCostPerToken: output,
|
|
953
|
+
source: "cost_map"
|
|
954
|
+
};
|
|
861
955
|
}
|
|
862
956
|
}
|
|
863
|
-
|
|
864
|
-
if (!entry) return null;
|
|
865
|
-
const input = entry.input_cost_per_token;
|
|
866
|
-
const output = entry.output_cost_per_token;
|
|
867
|
-
if (!bothFinite(input, output)) return null;
|
|
868
|
-
return {
|
|
869
|
-
inputCostPerToken: input,
|
|
870
|
-
outputCostPerToken: output,
|
|
871
|
-
source: "cost_map"
|
|
872
|
-
};
|
|
957
|
+
return null;
|
|
873
958
|
}
|
|
874
959
|
function priceTokens(priced, promptTokens, completionTokens) {
|
|
875
960
|
const p = Math.max(0, promptTokens);
|
|
@@ -894,6 +979,9 @@ var BudgetGuard = class {
|
|
|
894
979
|
lastCost = 0;
|
|
895
980
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
896
981
|
reserved = 0;
|
|
982
|
+
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
983
|
+
spendEvents = [];
|
|
984
|
+
maxLogEvents;
|
|
897
985
|
/**
|
|
898
986
|
* @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
|
|
899
987
|
*/
|
|
@@ -909,7 +997,13 @@ var BudgetGuard = class {
|
|
|
909
997
|
`nearLimitBps must be an integer in 0..10000, got ${nearLimitBps}`
|
|
910
998
|
);
|
|
911
999
|
}
|
|
1000
|
+
if (options.maxLogEvents !== void 0 && (!Number.isInteger(options.maxLogEvents) || options.maxLogEvents < 0)) {
|
|
1001
|
+
throw new RangeError(
|
|
1002
|
+
`maxLogEvents must be a non-negative integer, got ${options.maxLogEvents}`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
912
1005
|
this.limitUsd = limitUsd;
|
|
1006
|
+
this.maxLogEvents = options.maxLogEvents;
|
|
913
1007
|
this.priceOverrides = options.priceOverrides;
|
|
914
1008
|
this.failClosed = options.failClosed ?? true;
|
|
915
1009
|
this.onBlock = options.onBlock ?? defaultOnBlock;
|
|
@@ -971,7 +1065,10 @@ var BudgetGuard = class {
|
|
|
971
1065
|
* Release a reservation and record the actual cost. `record` is `settle` with
|
|
972
1066
|
* no reservation. Returns the USD cost of this call; unpriceable-model handling
|
|
973
1067
|
* matches {@link BudgetGuard.record}, and any held reservation is released even
|
|
974
|
-
* on the warn-and-skip path.
|
|
1068
|
+
* on the warn-and-skip path. A priced call appends one {@link SpendEvent} to
|
|
1069
|
+
* {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);
|
|
1070
|
+
* the warn-and-skip path accrues nothing and logs nothing, so the ledger stays
|
|
1071
|
+
* in lockstep with `spentUsd`.
|
|
975
1072
|
*/
|
|
976
1073
|
settle(model, promptTokens, completionTokens, options = {}) {
|
|
977
1074
|
const reserved = options.reserved ?? 0;
|
|
@@ -1008,6 +1105,18 @@ var BudgetGuard = class {
|
|
|
1008
1105
|
this.spentUsd = this.limitUsd;
|
|
1009
1106
|
}
|
|
1010
1107
|
this.lastCost = cost;
|
|
1108
|
+
this.appendEvent({
|
|
1109
|
+
timestamp: Date.now() / 1e3,
|
|
1110
|
+
kind: "llm",
|
|
1111
|
+
modelOrTool: model,
|
|
1112
|
+
promptTokens,
|
|
1113
|
+
completionTokens,
|
|
1114
|
+
costUsd: cost,
|
|
1115
|
+
...options.label !== void 0 ? { label: options.label } : {},
|
|
1116
|
+
// 0 means "no reservation" (the plain record() path) — omit rather than
|
|
1117
|
+
// log a meaningless zero.
|
|
1118
|
+
...reserved ? { reserved } : {}
|
|
1119
|
+
});
|
|
1011
1120
|
return cost;
|
|
1012
1121
|
}
|
|
1013
1122
|
/**
|
|
@@ -1020,9 +1129,40 @@ var BudgetGuard = class {
|
|
|
1020
1129
|
record(model, promptTokens, completionTokens, options = {}) {
|
|
1021
1130
|
return this.settle(model, promptTokens, completionTokens, {
|
|
1022
1131
|
reserved: 0,
|
|
1023
|
-
price: options.price
|
|
1132
|
+
price: options.price,
|
|
1133
|
+
label: options.label
|
|
1024
1134
|
});
|
|
1025
1135
|
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
|
|
1138
|
+
*
|
|
1139
|
+
* Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
|
|
1140
|
+
* same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
|
|
1141
|
+
* `check()` / `reserve()` see them) and appends a `kind: "tool"`
|
|
1142
|
+
* {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
|
|
1143
|
+
* cost: tools have no token usage to price. Deliberately does NOT update the
|
|
1144
|
+
* next-call estimate — that predicts the next *LLM* call, and a tool's price
|
|
1145
|
+
* would skew it. Returns `costUsd`.
|
|
1146
|
+
*/
|
|
1147
|
+
recordTool(tool, costUsd, options = {}) {
|
|
1148
|
+
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
1149
|
+
throw new RangeError(`costUsd must be a finite, non-negative number, got ${costUsd}`);
|
|
1150
|
+
}
|
|
1151
|
+
this.spentUsd += costUsd;
|
|
1152
|
+
if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
|
|
1153
|
+
this.spentUsd = this.limitUsd;
|
|
1154
|
+
}
|
|
1155
|
+
this.appendEvent({
|
|
1156
|
+
timestamp: Date.now() / 1e3,
|
|
1157
|
+
kind: "tool",
|
|
1158
|
+
modelOrTool: tool,
|
|
1159
|
+
promptTokens: null,
|
|
1160
|
+
completionTokens: null,
|
|
1161
|
+
costUsd,
|
|
1162
|
+
...options.label !== void 0 ? { label: options.label } : {}
|
|
1163
|
+
});
|
|
1164
|
+
return costUsd;
|
|
1165
|
+
}
|
|
1026
1166
|
/**
|
|
1027
1167
|
* Drop an in-flight reservation without recording spend (e.g. the call failed
|
|
1028
1168
|
* before producing usage). Safe to call with `0`.
|
|
@@ -1038,6 +1178,46 @@ var BudgetGuard = class {
|
|
|
1038
1178
|
get remainingUsd() {
|
|
1039
1179
|
return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);
|
|
1040
1180
|
}
|
|
1181
|
+
/**
|
|
1182
|
+
* The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
|
|
1183
|
+
* `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
|
|
1184
|
+
* it cannot corrupt the ledger.
|
|
1185
|
+
*/
|
|
1186
|
+
get spendLog() {
|
|
1187
|
+
return [...this.spendEvents];
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* The spend ledger as JSONL — one event per line, newline-terminated.
|
|
1191
|
+
*
|
|
1192
|
+
* The schema is stable and language-independent (snake_case keys, fixed order;
|
|
1193
|
+
* optional fields omitted when absent), identical to the Python package's
|
|
1194
|
+
* `export_log()`, so heterogeneous agents produce logs you can concatenate and
|
|
1195
|
+
* analyse as one stream. (The *schema* is the contract, not the bytes: the two
|
|
1196
|
+
* runtimes may render the same float differently, e.g. JS `0.0000025` vs
|
|
1197
|
+
* Python `2.5e-06`.) Empty ledger yields `""`.
|
|
1198
|
+
*/
|
|
1199
|
+
exportLog() {
|
|
1200
|
+
return this.spendEvents.map((e) => {
|
|
1201
|
+
const row = {
|
|
1202
|
+
timestamp: e.timestamp,
|
|
1203
|
+
kind: e.kind,
|
|
1204
|
+
model_or_tool: e.modelOrTool,
|
|
1205
|
+
prompt_tokens: e.promptTokens,
|
|
1206
|
+
completion_tokens: e.completionTokens,
|
|
1207
|
+
cost_usd: e.costUsd
|
|
1208
|
+
};
|
|
1209
|
+
if (e.label !== void 0) row.label = e.label;
|
|
1210
|
+
if (e.reserved !== void 0) row.reserved = e.reserved;
|
|
1211
|
+
return `${JSON.stringify(row)}
|
|
1212
|
+
`;
|
|
1213
|
+
}).join("");
|
|
1214
|
+
}
|
|
1215
|
+
appendEvent(event) {
|
|
1216
|
+
this.spendEvents.push(Object.freeze(event));
|
|
1217
|
+
if (this.maxLogEvents !== void 0 && this.spendEvents.length > this.maxLogEvents) {
|
|
1218
|
+
this.spendEvents.splice(0, this.spendEvents.length - this.maxLogEvents);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1041
1221
|
/**
|
|
1042
1222
|
* Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
|
|
1043
1223
|
*
|