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 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
- function bareModel(model) {
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 slash = m.lastIndexOf("/");
843
- return slash === -1 ? m : m.slice(slash + 1);
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 bare = bareModel(model);
850
- if (overrides) {
851
- const ov = overrides[bare] ?? overrides[model.trim()];
852
- if (ov !== void 0) {
853
- if (bothFinite(ov.inputCostPerToken, ov.outputCostPerToken)) {
854
- return {
855
- inputCostPerToken: ov.inputCostPerToken,
856
- outputCostPerToken: ov.outputCostPerToken,
857
- source: "override"
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
- return null;
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
- const entry = COST_MAP[bare] ?? COST_MAP[model.trim()];
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
  *