bantamkit-mcp 0.29.2 → 0.31.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.
@@ -15,8 +15,43 @@ export declare const VALID_TYPES: readonly ["feedback", "project", "reference",
15
15
  */
16
16
  export declare const DURABLE_TYPES: readonly ["feedback", "user"];
17
17
  export declare const DUPLICATE_JACCARD = 0.5;
18
+ export declare const RECALL_MIN_SCORE_RATIO = 0;
18
19
  /** See the reference's comment: measured against a real store, not chosen. */
19
20
  export declare const DEFAULT_INDEX_BUDGET = 24000;
21
+ /**
22
+ * Percent of the index budget that has to be SPENT before the store is called degraded.
23
+ *
24
+ * 90 and not 100 because the useful moment is before the refusal, not after it: at 100% the
25
+ * next `memory_save` has already failed and the operator has already seen the error. An
26
+ * INTEGER percent, compared by cross-multiplication where it is read, so the two runtimes
27
+ * cannot land on opposite sides of the line through a float they rounded differently.
28
+ *
29
+ * AMENDMENT (job46, J46-5, mirroring the reference's J46-4). It used to live in
30
+ * `mcp/status.ts`, next to the only thing that read it, and that is exactly what
31
+ * `docs/porting.md`'s register item 7 is about: the report warned at THIS line while the
32
+ * remedy it named — `compact` — aimed at a different one, so between the two the command
33
+ * exited 0 having archived nothing. `compact` has to know where the warning is to be able to
34
+ * clear it, and `mcp/status.ts` is above this layer, so the number moved DOWN to the layer
35
+ * both readers can reach. Same name, same value, same integer comparison; `mcp/status.ts`
36
+ * imports it and re-exports the name rather than spelling a second 90.
37
+ */
38
+ export declare const INDEX_PRESSURE_PERCENT = 90;
39
+ /**
40
+ * The largest index size `INDEX_PRESSURE_PERCENT` does NOT call degraded, in bytes.
41
+ *
42
+ * Integer arithmetic only, and the identity it holds is
43
+ * `size > undegradedIndexCeiling(b)` exactly when `size * 100 >= PERCENT * b` — the
44
+ * cross-multiplied comparison `mcp/status.ts`'s `indexPressureCondition` writes. The two
45
+ * spellings are pinned against each other by a boundary sweep in `test/server.test.mjs`,
46
+ * because they are two spellings and a test is the only thing that can keep them one line.
47
+ *
48
+ * `Math.floor` AND NOT `Math.trunc`, because the reference spells this `//` — floor division,
49
+ * which rounds toward -infinity — and `Math.trunc` rounds toward zero. The two differ on
50
+ * every negative budget, and a budget is not guaranteed positive by anything in this
51
+ * function's own signature. `test/store.test.mjs` sweeps both signs against a BigInt oracle
52
+ * rather than against a second copy of this expression.
53
+ */
54
+ export declare function undegradedIndexCeiling(budget: number): number;
20
55
  export declare class MemoryValidationError extends BantamError {
21
56
  }
22
57
  export declare class MemoryBudgetExceeded extends BantamError {
@@ -55,6 +90,17 @@ export interface CompactResult {
55
90
  headroom: number;
56
91
  archiveDir: string;
57
92
  }
93
+ /**
94
+ * The store members `dream.ts` needs and no caller outside it should have. See
95
+ * `MemoryStore.internals`.
96
+ */
97
+ export interface StoreInternals {
98
+ facts(): Fact[];
99
+ factPath(name: FactValue): string;
100
+ indexLine(fact: Fact): string;
101
+ writeFact(fact: Fact): void;
102
+ rebuildIndex(): void;
103
+ }
58
104
  export interface MemoryStoreOptions {
59
105
  indexBudget?: number;
60
106
  k?: number;
@@ -186,8 +232,25 @@ export declare class MemoryStore {
186
232
  * `[]` — the same answer as a real miss, which `component.Memory` used to turn into "no
187
233
  * memories matched. Try different words", telling a person to rephrase a question at a
188
234
  * filing cabinet nobody could open.
235
+ *
236
+ * `minRatio` is roadmap #6's precision gate: keep a fact only if its score is at least
237
+ * that fraction of the BEST score in this same recall. The default is
238
+ * `RECALL_MIN_SCORE_RATIO`, which is 0.0 and gates nothing — see the paragraph at that
239
+ * constant for why the number is a no-op today and what has to be measured before it
240
+ * stops being one. The comparison is relative to this store's own best because that is
241
+ * the only quantity in reach here that cancels query length; a layered `Memory` therefore
242
+ * applies the gate once per layer, against each layer's own top hit, and never across
243
+ * layers.
244
+ *
245
+ * THE RANGE CHECK RUNS BEFORE ANY FILE IS READ. A ratio outside `[0.0, 1.0]` is a
246
+ * caller's bug, and reporting it as "your store has no matches" would send someone to
247
+ * look at their memories for a defect that is in the argument. It is spelled
248
+ * `!(minRatio >= 0 && minRatio <= 1)` and NOT `(minRatio < 0 || minRatio > 1)`: every
249
+ * comparison against `NaN` is false, so the second spelling ADMITS `NaN` where the
250
+ * reference's `not 0.0 <= min_ratio <= 1.0` refuses it. That is a real mutant (M7b) and
251
+ * it is killed on both sides.
189
252
  */
190
- recall(query: string, k?: number | null, stamp?: boolean): Fact[];
253
+ recall(query: string, k?: number | null, stamp?: boolean, minRatio?: number): Fact[];
191
254
  /**
192
255
  * `len(store._facts())` at the operator CLI, which is the only caller.
193
256
  *
@@ -216,6 +279,28 @@ export declare class MemoryStore {
216
279
  * store currently holds (capped at half the budget), so the headroom bought is "a fact as
217
280
  * big as your biggest one will fit", measured from this store's own data.
218
281
  *
282
+ * AMENDMENT (job46, J46-5, the port of the reference's J46-4), and it supersedes the
283
+ * sentence above about where that reserve is measured FROM. WAS: `budget - largest index
284
+ * line`. NOW: that same largest line plus the headroom `INDEX_PRESSURE_PERCENT` demands, so
285
+ * the target is `undegradedIndexCeiling(budget) - largest index line`. THE REASON IS
286
+ * `docs/porting.md`'s register item 7: the degraded report warns at 90% of the budget and
287
+ * names THIS command, while the old target sat at `budget - largest line`, so on any store
288
+ * whose biggest line is under a tenth of its budget the command the operator was told to
289
+ * run archived nothing and the warning stayed up. MEASURED on a read-only copy of this
290
+ * machine's project store (101 facts, index.md 21819 bytes of 24000 = 90.91%, largest index
291
+ * line 361 bytes, so the old target was 23639 = 98.50%): `compact()` answered
292
+ * `archived: []` and `index-budget-low` was still firing afterwards — the same two answers
293
+ * the reference gave on the same store, which is why the register calls this a defect of
294
+ * the reference and NOT a divergence. The paragraph above is why the fix is a substitution
295
+ * and not a new number — "a loop that stopped at 'fits' would archive nothing at the only
296
+ * moment the remedy is ever named" is the same argument one line lower down, so the reserve
297
+ * is measured from the line the WARNING draws instead of the one the REFUSAL draws.
298
+ *
299
+ * WHAT THIS DOES NOT CHANGE, deliberately: the eviction ORDER (`byEviction`), the
300
+ * half-the-budget cap, and an EXPLICIT `reserve`. A caller that passes one gets the
301
+ * arithmetic it always got, byte for byte — every eviction-order node in `store.test.mjs`
302
+ * passes one for exactly that reason, and so may any caller that needs the old default back.
303
+ *
219
304
  * `facts()` runs FIRST, before any rename, so an unreadable store moves nothing.
220
305
  *
221
306
  * THE MOVE IS `os.replace`, AND THE REFERENCE'S IS NOW TOO — the difference this comment
@@ -388,6 +473,21 @@ export declare class MemoryStore {
388
473
  * Registered as a runtime-py defect, not fixed here (invariant 8).
389
474
  */
390
475
  indexText(): string;
476
+ /**
477
+ * The `_`-prefixed members `dream.ts` reaches for, handed out as ONE named seam.
478
+ *
479
+ * The reference's `dream.py` calls `store._facts()`, `_fact_path`, `_index_line`,
480
+ * `_write_fact` and `_rebuild_index` directly — Python's underscore is a convention and
481
+ * costs nothing to cross. TypeScript's `private` is enforced, so the port needs a door,
482
+ * and this is deliberately one door rather than five relaxed modifiers: it is greppable,
483
+ * it names its one caller, and a sixth internal does not become reachable by accident.
484
+ *
485
+ * `_fact_text` has no member here because it never needed splitting out on this side —
486
+ * `formatFact` in `factfile.ts` has always been the serialiser `writeFact` calls, so the
487
+ * bytes `dream` MEASURES and the bytes the store WRITES are already one function. That is
488
+ * the code-shape difference the Python half had to manufacture, not a behaviour one.
489
+ */
490
+ internals(): StoreInternals;
391
491
  private factPath;
392
492
  private indexLine;
393
493
  /**
@@ -68,8 +68,93 @@ export const DURABLE_TYPES = ['feedback', 'user'];
68
68
  const NAME_RE = /^[a-z0-9][a-z0-9-]*\n?$/;
69
69
  const NAME_PATTERN = '^[a-z0-9][a-z0-9-]*$';
70
70
  export const DUPLICATE_JACCARD = 0.5;
71
+ // ---- the precision gate (roadmap #6) ---------------------------------------------
72
+ //
73
+ // `recall` keeps a fact whose score is at least `minRatio` of the BEST score in the same
74
+ // recall. WHY THIS NUMBER IS 0.0, and what will replace it:
75
+ //
76
+ // 0.0 IS A DELIBERATE NO-OP, not a tuned value. `score >= 0.0 * best` is true for every
77
+ // fact `recall` was ever going to return — the scoring loop keeps only `score > 0` — so
78
+ // shipping this gate at its default changes not one recall, not one injected header, and
79
+ // not one byte of any reply. That is the whole point. The threshold that would actually
80
+ // cut something has to come from `tools/ledger/injection-precision.mjs`, and on the day
81
+ // that tool shipped (2026-09-06) it REFUSED to report a rate: 491 injection records, 3 of
82
+ // them carrying names+scores+session, 2 distinct sessions, and no control arm, against a
83
+ // floor of 100 joinable injections across 5 sessions. There is no retroactive baseline —
84
+ // the 488 older records carry `hits` and `bytes` and nothing joinable — so a number chosen
85
+ // today would be a number chosen off three rows, shipped as a silent suppressor of memory
86
+ // injection. The mechanism lands now; the number lands when that tool answers instead of
87
+ // refusing.
88
+ //
89
+ // WHY A RATIO AND NOT A COUNT. `score` is an unnormalised intersection size,
90
+ // `len(tokens(name + " " + description) & tokens(query))`, so it scales with how long the
91
+ // QUERY is. Measured on the three instrumented records: the same two-fact shape scored 2
92
+ // and 2 on a 452-character prompt, 4 and 4 on a 453-character prompt, and 22 and 21 on a
93
+ // 7855-character one. An absolute cut of, say, 5 would gate out both short prompts
94
+ // entirely and admit everything on the long one, which is a rule about prompt length
95
+ // wearing a relevance costume. Within ONE recall the query is fixed, so dividing by the
96
+ // best score in that same recall cancels the length term exactly. Jaccard was the other
97
+ // candidate and was rejected for the mirror-image bias: `|a & b| / |a | b|` puts the
98
+ // query's own token count in the denominator, so it would gate out LONG prompts instead.
99
+ //
100
+ // THE GATE IS RELATIVE, SO IT CAN NEVER EMPTY A RECALL. The top hit IS the max, so
101
+ // `best >= minRatio * best` holds for every ratio in range, 1.0 included. It narrows an
102
+ // injection; it never suppresses one, and the COUNT of prompts that get an injection is
103
+ // invariant at every setting.
104
+ //
105
+ // WHAT THE RATIO DOES NOT FIX: `tokens` is `re.findall(r"[a-z0-9]+", text.lower())`,
106
+ // ASCII-only. A wholly non-Latin prompt tokenises to the empty set and scores 0 against
107
+ // every fact, so it never reaches this gate at all — it is already an empty recall. This
108
+ // store's operator writes Thai; a threshold tuned on English prompts would be tuned on a
109
+ // population that structurally excludes theirs.
110
+ //
111
+ // The number lives in TWO places, one per runtime, and they must hold the SAME float:
112
+ // `runtime-py/src/bantamkit/memory/store.py`'s `RECALL_MIN_SCORE_RATIO` is the reference.
113
+ // `Memory.recallOutcome` and the `UserPromptSubmit` hook that calls it inherit it rather
114
+ // than restating it.
115
+ export const RECALL_MIN_SCORE_RATIO = 0.0;
116
+ // Spelled once because both runtimes raise it verbatim. The offending value is NOT
117
+ // interpolated: Python renders `2.0` as `2.0` and JavaScript renders it as `2`, so a
118
+ // sentence carrying the number would be a divergence manufactured by float formatting.
119
+ const MIN_RATIO_RANGE = 'recall min-score ratio must be between 0.0 and 1.0';
71
120
  /** See the reference's comment: measured against a real store, not chosen. */
72
121
  export const DEFAULT_INDEX_BUDGET = 24_000;
122
+ /**
123
+ * Percent of the index budget that has to be SPENT before the store is called degraded.
124
+ *
125
+ * 90 and not 100 because the useful moment is before the refusal, not after it: at 100% the
126
+ * next `memory_save` has already failed and the operator has already seen the error. An
127
+ * INTEGER percent, compared by cross-multiplication where it is read, so the two runtimes
128
+ * cannot land on opposite sides of the line through a float they rounded differently.
129
+ *
130
+ * AMENDMENT (job46, J46-5, mirroring the reference's J46-4). It used to live in
131
+ * `mcp/status.ts`, next to the only thing that read it, and that is exactly what
132
+ * `docs/porting.md`'s register item 7 is about: the report warned at THIS line while the
133
+ * remedy it named — `compact` — aimed at a different one, so between the two the command
134
+ * exited 0 having archived nothing. `compact` has to know where the warning is to be able to
135
+ * clear it, and `mcp/status.ts` is above this layer, so the number moved DOWN to the layer
136
+ * both readers can reach. Same name, same value, same integer comparison; `mcp/status.ts`
137
+ * imports it and re-exports the name rather than spelling a second 90.
138
+ */
139
+ export const INDEX_PRESSURE_PERCENT = 90;
140
+ /**
141
+ * The largest index size `INDEX_PRESSURE_PERCENT` does NOT call degraded, in bytes.
142
+ *
143
+ * Integer arithmetic only, and the identity it holds is
144
+ * `size > undegradedIndexCeiling(b)` exactly when `size * 100 >= PERCENT * b` — the
145
+ * cross-multiplied comparison `mcp/status.ts`'s `indexPressureCondition` writes. The two
146
+ * spellings are pinned against each other by a boundary sweep in `test/server.test.mjs`,
147
+ * because they are two spellings and a test is the only thing that can keep them one line.
148
+ *
149
+ * `Math.floor` AND NOT `Math.trunc`, because the reference spells this `//` — floor division,
150
+ * which rounds toward -infinity — and `Math.trunc` rounds toward zero. The two differ on
151
+ * every negative budget, and a budget is not guaranteed positive by anything in this
152
+ * function's own signature. `test/store.test.mjs` sweeps both signs against a BigInt oracle
153
+ * rather than against a second copy of this expression.
154
+ */
155
+ export function undegradedIndexCeiling(budget) {
156
+ return Math.floor((INDEX_PRESSURE_PERCENT * budget - 1) / 100);
157
+ }
73
158
  // The second half of the "unreadable" sentence, one per directory this store lists — and
74
159
  // then the same distinction one syscall down, for the stats `restore` does instead of a
75
160
  // listing. They are separate strings because the failures do different damage, and an error
@@ -494,11 +579,30 @@ export class MemoryStore {
494
579
  * `[]` — the same answer as a real miss, which `component.Memory` used to turn into "no
495
580
  * memories matched. Try different words", telling a person to rephrase a question at a
496
581
  * filing cabinet nobody could open.
582
+ *
583
+ * `minRatio` is roadmap #6's precision gate: keep a fact only if its score is at least
584
+ * that fraction of the BEST score in this same recall. The default is
585
+ * `RECALL_MIN_SCORE_RATIO`, which is 0.0 and gates nothing — see the paragraph at that
586
+ * constant for why the number is a no-op today and what has to be measured before it
587
+ * stops being one. The comparison is relative to this store's own best because that is
588
+ * the only quantity in reach here that cancels query length; a layered `Memory` therefore
589
+ * applies the gate once per layer, against each layer's own top hit, and never across
590
+ * layers.
591
+ *
592
+ * THE RANGE CHECK RUNS BEFORE ANY FILE IS READ. A ratio outside `[0.0, 1.0]` is a
593
+ * caller's bug, and reporting it as "your store has no matches" would send someone to
594
+ * look at their memories for a defect that is in the argument. It is spelled
595
+ * `!(minRatio >= 0 && minRatio <= 1)` and NOT `(minRatio < 0 || minRatio > 1)`: every
596
+ * comparison against `NaN` is false, so the second spelling ADMITS `NaN` where the
597
+ * reference's `not 0.0 <= min_ratio <= 1.0` refuses it. That is a real mutant (M7b) and
598
+ * it is killed on both sides.
497
599
  */
498
- recall(query, k = null, stamp = true) {
600
+ recall(query, k = null, stamp = true, minRatio = RECALL_MIN_SCORE_RATIO) {
601
+ if (!(minRatio >= 0 && minRatio <= 1))
602
+ throw new MemoryValidationError(MIN_RATIO_RANGE);
499
603
  const limit = k ?? this.k;
500
604
  const q = tokens(query);
501
- const scored = [];
605
+ let scored = [];
502
606
  for (const fact of this.facts()) {
503
607
  let score = 0;
504
608
  for (const t of tokens(`${pyText(fact.name)} ${pyText(fact.description)}`))
@@ -507,6 +611,20 @@ export class MemoryStore {
507
611
  if (score > 0)
508
612
  scored.push({ score, name: fact.name, fact });
509
613
  }
614
+ if (scored.length > 0) {
615
+ // No `if (minRatio > 0)` shortcut on purpose: at 0.0 this line still runs and still
616
+ // keeps everything, so "the default gates nothing" is a fact about the arithmetic
617
+ // rather than about a branch that could be edited away. `Math.max(...scored)` is NOT
618
+ // used: a spread is one argument per fact and a large store would overflow the
619
+ // argument limit, which is a crash the reference's `max(...)` over a generator does
620
+ // not have.
621
+ let best = scored[0].score;
622
+ for (const s of scored)
623
+ if (s.score > best)
624
+ best = s.score;
625
+ const floor = minRatio * best;
626
+ scored = scored.filter((s) => s.score >= floor);
627
+ }
510
628
  // `key=lambda pair: (-pair[0], pair[1].name)`. The name comparison is CODEPOINT order
511
629
  // for two strings — which is not what `Array.prototype.sort` does — and Python's `<`
512
630
  // for everything else, refusals included. See `sortScored`.
@@ -553,6 +671,28 @@ export class MemoryStore {
553
671
  * store currently holds (capped at half the budget), so the headroom bought is "a fact as
554
672
  * big as your biggest one will fit", measured from this store's own data.
555
673
  *
674
+ * AMENDMENT (job46, J46-5, the port of the reference's J46-4), and it supersedes the
675
+ * sentence above about where that reserve is measured FROM. WAS: `budget - largest index
676
+ * line`. NOW: that same largest line plus the headroom `INDEX_PRESSURE_PERCENT` demands, so
677
+ * the target is `undegradedIndexCeiling(budget) - largest index line`. THE REASON IS
678
+ * `docs/porting.md`'s register item 7: the degraded report warns at 90% of the budget and
679
+ * names THIS command, while the old target sat at `budget - largest line`, so on any store
680
+ * whose biggest line is under a tenth of its budget the command the operator was told to
681
+ * run archived nothing and the warning stayed up. MEASURED on a read-only copy of this
682
+ * machine's project store (101 facts, index.md 21819 bytes of 24000 = 90.91%, largest index
683
+ * line 361 bytes, so the old target was 23639 = 98.50%): `compact()` answered
684
+ * `archived: []` and `index-budget-low` was still firing afterwards — the same two answers
685
+ * the reference gave on the same store, which is why the register calls this a defect of
686
+ * the reference and NOT a divergence. The paragraph above is why the fix is a substitution
687
+ * and not a new number — "a loop that stopped at 'fits' would archive nothing at the only
688
+ * moment the remedy is ever named" is the same argument one line lower down, so the reserve
689
+ * is measured from the line the WARNING draws instead of the one the REFUSAL draws.
690
+ *
691
+ * WHAT THIS DOES NOT CHANGE, deliberately: the eviction ORDER (`byEviction`), the
692
+ * half-the-budget cap, and an EXPLICIT `reserve`. A caller that passes one gets the
693
+ * arithmetic it always got, byte for byte — every eviction-order node in `store.test.mjs`
694
+ * passes one for exactly that reason, and so may any caller that needs the old default back.
695
+ *
556
696
  * `facts()` runs FIRST, before any rename, so an unreadable store moves nothing.
557
697
  *
558
698
  * THE MOVE IS `os.replace`, AND THE REFERENCE'S IS NOW TOO — the difference this comment
@@ -607,8 +747,19 @@ export class MemoryStore {
607
747
  sizes.set(pyHashKey(fact.name), Buffer.byteLength(this.indexLine(fact), 'utf8'));
608
748
  }
609
749
  const all = [...sizes.values()];
610
- if (reserve === null)
611
- reserve = all.length === 0 ? 0 : Math.max(...all);
750
+ if (reserve === null) {
751
+ // The default reserve is measured from the WARNING LINE, not from the budget. One
752
+ // substitution, and it is the whole of `docs/porting.md` item 7: the sentence that
753
+ // names this command fires at `INDEX_PRESSURE_PERCENT`, so a remedy that only reaches
754
+ // `budget - largest line` is a no-op everywhere between them. `undegradedIndexCeiling`
755
+ // is that line; `+ largest line` keeps this method's own promise on the other side of
756
+ // it, unchanged in words: "a fact as big as your biggest one will fit" — before the
757
+ // index is degraded AGAIN, rather than before it is over budget.
758
+ reserve =
759
+ this.indexBudget -
760
+ undegradedIndexCeiling(this.indexBudget) +
761
+ (all.length === 0 ? 0 : Math.max(...all));
762
+ }
612
763
  // `min(reserve, self.index_budget // 2)`: floor division, and the budget is >= 1 here
613
764
  // because both CLIs refuse a smaller one at the edge.
614
765
  reserve = Math.max(0, Math.min(reserve, Math.floor(this.indexBudget / 2)));
@@ -839,6 +990,29 @@ export class MemoryStore {
839
990
  indexText() {
840
991
  return this.facts().map((fact) => this.indexLine(fact)).join('');
841
992
  }
993
+ /**
994
+ * The `_`-prefixed members `dream.ts` reaches for, handed out as ONE named seam.
995
+ *
996
+ * The reference's `dream.py` calls `store._facts()`, `_fact_path`, `_index_line`,
997
+ * `_write_fact` and `_rebuild_index` directly — Python's underscore is a convention and
998
+ * costs nothing to cross. TypeScript's `private` is enforced, so the port needs a door,
999
+ * and this is deliberately one door rather than five relaxed modifiers: it is greppable,
1000
+ * it names its one caller, and a sixth internal does not become reachable by accident.
1001
+ *
1002
+ * `_fact_text` has no member here because it never needed splitting out on this side —
1003
+ * `formatFact` in `factfile.ts` has always been the serialiser `writeFact` calls, so the
1004
+ * bytes `dream` MEASURES and the bytes the store WRITES are already one function. That is
1005
+ * the code-shape difference the Python half had to manufacture, not a behaviour one.
1006
+ */
1007
+ internals() {
1008
+ return {
1009
+ facts: () => this.facts(),
1010
+ factPath: (name) => this.factPath(name),
1011
+ indexLine: (fact) => this.indexLine(fact),
1012
+ writeFact: (fact) => this.writeFact(fact),
1013
+ rebuildIndex: () => this.rebuildIndex(),
1014
+ };
1015
+ }
842
1016
  // ---- internals ----
843
1017
  factPath(name) {
844
1018
  return pyJoin(this.root, 'facts', `${pyText(name)}.md`);
@@ -0,0 +1,100 @@
1
+ import { BantamError } from './errors.js';
2
+ export declare const PRICES_ENV = "BANTAMKIT_PRICES";
3
+ export declare const PRICE_SCHEMA_VERSION = 1;
4
+ export declare const CURRENCY = "USD";
5
+ export declare const RATE_UNIT = "micro_usd_per_million_tokens";
6
+ /** A rate is micro-USD per this many tokens. */
7
+ export declare const TOKENS_PER_RATE_UNIT = 1000000;
8
+ /** Micro-USD in one USD. Also the rounding divisor, hence `HALF` below. */
9
+ export declare const MICROS_PER_UNIT = 1000000;
10
+ /** The largest integer both runtimes represent exactly. */
11
+ export declare const MAX_SAFE_INT: number;
12
+ /**
13
+ * The four classes the host's own `usage` block distinguishes, in the order the ledger
14
+ * accumulates them. This tuple is the unit of pricing; see property 1 in the reference.
15
+ */
16
+ export declare const TOKEN_CLASSES: readonly ["input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens", "output_tokens"];
17
+ export type TokenClass = (typeof TOKEN_CLASSES)[number];
18
+ export interface Rate {
19
+ recorded: string;
20
+ source: string;
21
+ [cls: string]: string | number;
22
+ }
23
+ export interface PriceTable {
24
+ schema_version: number;
25
+ currency: string;
26
+ unit: string;
27
+ note?: string;
28
+ rates: Record<string, Rate>;
29
+ }
30
+ /** A cost, or a refusal. Never both, and never a zero standing in for a refusal. */
31
+ export type PriceResult = {
32
+ unavailable: string;
33
+ } | {
34
+ model: string;
35
+ currency: string;
36
+ micros: number;
37
+ amount: string;
38
+ breakdown: Record<string, number>;
39
+ rate: Rate;
40
+ };
41
+ /**
42
+ * The table on disk is malformed.
43
+ *
44
+ * This THROWS rather than returning `{unavailable}`, and the split is deliberate: a broken
45
+ * price table is an operator configuration fault that must stop and name itself, while an
46
+ * unpriced model is a normal, expected answer that a caller keeps working past.
47
+ */
48
+ export declare class PriceTableError extends BantamError {
49
+ }
50
+ /**
51
+ * `$BANTAMKIT_PRICES` verbatim if set, else the shipped table in the asset pack.
52
+ *
53
+ * The override wins even when it is wrong -- same arm order and same reasoning as
54
+ * `assetsRoot()`: an override that points nowhere must fail naming itself rather than fall
55
+ * through to a table the operator did not ask for.
56
+ */
57
+ export declare function priceTablePath(): string;
58
+ /**
59
+ * Read and validate a price table. `path` is used VERBATIM in every message it appears in,
60
+ * never normalised, because `Path('./x')` is `x` on the reference side and `./x` here and the
61
+ * fault sentence would then differ over a path neither runtime chose.
62
+ */
63
+ export declare function loadPriceTable(path?: string): PriceTable;
64
+ /**
65
+ * The decode both runtimes must agree on, in one place so the gate can reach it.
66
+ *
67
+ * This side is already the strict one -- `JSON.parse` rejects the bare `NaN` / `Infinity` /
68
+ * `-Infinity` tokens that `json.loads` accepts -- so the function is a plain `JSON.parse`
69
+ * and the reference is what had to move. It exists as a named arm anyway, because a rule
70
+ * that lives in only one of two runtimes is a rule the conformance harness cannot reach.
71
+ */
72
+ export declare function decodePriceJson(text: string): unknown;
73
+ /**
74
+ * Every rule the table must satisfy, each with its own sentence.
75
+ *
76
+ * Returns a NORMALISED table: the top-level keys in a fixed order, rate names in code-point
77
+ * order, and every count narrowed. See the reference for the ONE thing normalisation cannot
78
+ * buy -- the iteration order of `rates` for a model name JavaScript reads as an array index.
79
+ */
80
+ export declare function validatePriceTable(raw: unknown): PriceTable;
81
+ /**
82
+ * Micro-USD as a fixed-6-decimal string, built from integers only.
83
+ *
84
+ * Non-negative input only; every producer in this module is a sum of non-negative products.
85
+ */
86
+ export declare function formatMicros(micros: number): string;
87
+ /**
88
+ * The cost of one `usage` block under one model's rate, or a named refusal.
89
+ *
90
+ * `table` must be a table `validatePriceTable` returned.
91
+ *
92
+ * THE ORDER OF THE CHECKS IS PART OF THE CONTRACT. The usage is validated BEFORE the model is
93
+ * looked up, so a caller who passed a malformed usage is told that, rather than being sent to
94
+ * edit a price table over a bug in the call.
95
+ *
96
+ * THE USAGE MUST CARRY ALL FOUR CLASSES AND NOTHING ELSE. See the reference: a token class
97
+ * silently ignored is money silently dropped, and a class silently defaulted to zero is money
98
+ * silently invented.
99
+ */
100
+ export declare function priceTokens(table: PriceTable, model: string, usage: unknown): PriceResult;