@vincemakes/kiso-runtime 0.1.37 → 0.2.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.
@@ -0,0 +1,237 @@
1
+ /**
2
+ * E1 (1.2.0) — the Request Trace record schema (proposal §1.1 as adopted
3
+ * by the R1 ruling: "adopt as-is, keep ts, per-round additive"). This is
4
+ * the 1.2.0 field-set lock: every field below is spec'd, and the
5
+ * closed-field-set gate (ruling R1a) pins the shape bidirectionally —
6
+ * `TRACE_RECORD_FIELDS`/`TRACE_SEGMENT_FIELDS` must stay in exact
7
+ * agreement with the interfaces, or trace-schema.test.ts goes red.
8
+ *
9
+ * R1a: `schemaVersion` pins BOTH the record shape AND the hash /
10
+ * fingerprint algorithms (`HASH_SPEC_BY_VERSION`). A version bump must
11
+ * pin a spec for the new version before any writer can use it
12
+ * (`hashSpecFor` throws otherwise).
13
+ *
14
+ * The ledger is OUT-side (ADR-0051 §6): versionable, never part of the
15
+ * correctness ABI. These types are runtime-internal.
16
+ *
17
+ * E2 (1.3.0) — schemaVersion 2: the record gains the `canonical` block
18
+ * (E2 proposal §1.5 Case A — the nested block, ruled 2026-08-13; raw
19
+ * quartet stays as provider observation above it). The validators accept
20
+ * BOTH generations (R1d-1): a v1 sidecar has no canonical block and reads
21
+ * as defaults at every consumer — never a crash.
22
+ */
23
+ /** schemaVersion: 2 for 1.3.0 (the canonical block). Version 1 = the 1.2.0
24
+ * shape, kept for generation-compat reads (R1d-1). Algorithm and shape
25
+ * changes bump it (ADR-0051 §6 OUT-side versioning). */
26
+ export const TRACE_SCHEMA_VERSION = 2;
27
+ /** The versions a reader may meet in a ledger. v1 records are accepted
28
+ * (generation-compat, R1d-1) and read as defaults — no canonical block. */
29
+ export const TRACE_SCHEMA_VERSIONS = new Set([1, TRACE_SCHEMA_VERSION]);
30
+ import { PRICING_TABLE_V1, priceFor, pricingTableFor, validateCanonicalUsage } from "../usage/canonical.js";
31
+ export const HASH_SPEC_BY_VERSION = {
32
+ 1: { algorithm: "sha-256", output: "full-hex" },
33
+ 2: { algorithm: "sha-256", output: "full-hex" }, // E2 — the algorithms do not change
34
+ };
35
+ export function hashSpecFor(version) {
36
+ const spec = HASH_SPEC_BY_VERSION[version];
37
+ if (spec === undefined)
38
+ throw new Error(`no hash spec pinned for trace schemaVersion ${version}`);
39
+ return spec;
40
+ }
41
+ // ── The closed-field-set gate (R1a) ───────────────────────────────────────
42
+ // Trace-schema.test.ts asserts Object.keys of a fully populated record is
43
+ // EXACTLY this set (both directions).
44
+ /** The 1.2.0 field set (schemaVersion 1) — kept verbatim for
45
+ * generation-compat reads of old sidecars (R1d-1): a v1 record has no
46
+ * canonical block and reads as defaults at every consumer. */
47
+ export const TRACE_RECORD_FIELDS_V1 = [
48
+ "schemaVersion",
49
+ "kind",
50
+ "requestId",
51
+ "runId",
52
+ "requestIndex",
53
+ "retryAttempt",
54
+ "provider",
55
+ "model",
56
+ "adapterVersion",
57
+ "systemPromptHash",
58
+ "toolSchemaHash",
59
+ "contextHash",
60
+ "contextManifest",
61
+ "segmentHashes",
62
+ "stablePrefixFingerprint",
63
+ "freshInput",
64
+ "cacheRead",
65
+ "cacheWrite",
66
+ "output",
67
+ "latencyMs",
68
+ "ttftMs",
69
+ "toolCalls",
70
+ "outcome",
71
+ "lineageLink",
72
+ "ts",
73
+ ];
74
+ /** The 1.3.0 field set (schemaVersion 2) = the v1 set + `canonical`. */
75
+ export const TRACE_RECORD_FIELDS = [...TRACE_RECORD_FIELDS_V1, "canonical"];
76
+ export const TRACE_SEGMENT_FIELDS = ["role", "seqRange", "estTokens", "freshness"];
77
+ // ── Validators ────────────────────────────────────────────────────────────
78
+ // Strict by design: extra keys are rejected (the closed set), so a
79
+ // misspelled field can never silently enter the ledger.
80
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
81
+ const keysOf = (v) => (isRecord(v) ? Object.keys(v) : []);
82
+ const isNonNegInt = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
83
+ const isNumber = (v) => typeof v === "number" && Number.isFinite(v);
84
+ const isHex64 = (v) => typeof v === "string" && /^[0-9a-f]{64}$/.test(v);
85
+ /** The closed set, both directions: no key outside the spec, and every
86
+ * spec'd key present except the explicit optional ones. */
87
+ const hasClosedKeys = (v, spec, optional = []) => {
88
+ const keys = keysOf(v);
89
+ if (keys.some((k) => !spec.includes(k)))
90
+ return false;
91
+ const optionalSet = new Set(optional);
92
+ return spec.every((k) => optionalSet.has(k) || keys.includes(k));
93
+ };
94
+ const VALID_SEGMENT_ROLES = new Set(["system", "tools", "turn", "current_turn"]);
95
+ const VALID_FRESHNESS = new Set(["fresh", "cache_read", "cache_write"]);
96
+ const VALID_OUTCOMES = new Set(["ok", "provider_error", "aborted"]);
97
+ function isValidSeqRange(v) {
98
+ if (v === null)
99
+ return true;
100
+ if (!Array.isArray(v) || v.length !== 2)
101
+ return false;
102
+ const [a, b] = v;
103
+ return isNonNegInt(a) && isNonNegInt(b) && a <= b;
104
+ }
105
+ export function validateTraceSegment(v) {
106
+ if (!isRecord(v) || !hasClosedKeys(v, TRACE_SEGMENT_FIELDS))
107
+ return false;
108
+ if (typeof v.role !== "string" || !VALID_SEGMENT_ROLES.has(v.role))
109
+ return false;
110
+ if (!isValidSeqRange(v.seqRange))
111
+ return false;
112
+ if (!isNonNegInt(v.estTokens))
113
+ return false;
114
+ if (!VALID_FRESHNESS.has(v.freshness))
115
+ return false;
116
+ return true;
117
+ }
118
+ export function validateTraceRecord(v) {
119
+ if (!isRecord(v))
120
+ return false;
121
+ const version = v.schemaVersion;
122
+ // generation-compat (R1d-1): a v1 sidecar has no canonical block and
123
+ // reads as defaults — accepted, never a crash; the current version is
124
+ // fully checked (shape + the canonical block + its consistency).
125
+ if (version !== 1 && version !== TRACE_SCHEMA_VERSION)
126
+ return false;
127
+ const fields = version === 1 ? TRACE_RECORD_FIELDS_V1 : TRACE_RECORD_FIELDS;
128
+ if (!hasClosedKeys(v, fields, ["lineageLink"]))
129
+ return false;
130
+ if (v.kind !== "request")
131
+ return false;
132
+ if (typeof v.requestId !== "string" || typeof v.runId !== "string")
133
+ return false;
134
+ if (!isNonNegInt(v.requestIndex) || !isNonNegInt(v.retryAttempt))
135
+ return false;
136
+ if (typeof v.provider !== "string" || typeof v.model !== "string")
137
+ return false;
138
+ if (v.adapterVersion !== null && typeof v.adapterVersion !== "string")
139
+ return false;
140
+ if (!isHex64(v.systemPromptHash) || !isHex64(v.toolSchemaHash) || !isHex64(v.contextHash))
141
+ return false;
142
+ if (!isHex64(v.stablePrefixFingerprint))
143
+ return false;
144
+ if (!Array.isArray(v.contextManifest) || !v.contextManifest.every(validateTraceSegment))
145
+ return false;
146
+ // segmentHashes must mirror the manifest 1:1 — a misaligned list
147
+ // would silently corrupt the break derivation (R4b)
148
+ if (!Array.isArray(v.segmentHashes) ||
149
+ v.segmentHashes.length !== v.contextManifest.length ||
150
+ !v.segmentHashes.every(isHex64))
151
+ return false;
152
+ if (!isNumber(v.freshInput) || !isNumber(v.cacheRead))
153
+ return false;
154
+ if (v.cacheWrite !== null && !isNumber(v.cacheWrite))
155
+ return false;
156
+ if (!isNumber(v.output) || !isNumber(v.latencyMs))
157
+ return false;
158
+ if (!isNumber(v.ttftMs))
159
+ return false; // 0 = unknown, never null (locked set)
160
+ if (!Array.isArray(v.toolCalls) || !v.toolCalls.every((t) => typeof t === "string"))
161
+ return false;
162
+ if (typeof v.outcome !== "string" || !VALID_OUTCOMES.has(v.outcome))
163
+ return false;
164
+ if (v.lineageLink !== undefined) {
165
+ const l = v.lineageLink;
166
+ if (!isRecord(l))
167
+ return false;
168
+ if (typeof l.parentSessionId !== "string" || typeof l.parentRunId !== "string")
169
+ return false;
170
+ if (!isNonNegInt(l.parentInvocationSeq))
171
+ return false;
172
+ if (typeof l.role !== "string")
173
+ return false;
174
+ }
175
+ if (version !== 1) {
176
+ // the canonical block: the schema's invariants machine-checked
177
+ if (!validateCanonicalUsage(v.canonical))
178
+ return false;
179
+ const c = v.canonical;
180
+ // the block formalizes the raw quartet — a divergence means the
181
+ // derivation drifted (a future bug, caught at the ledger)
182
+ if (c.input !== v.freshInput || c.cacheRead !== v.cacheRead || c.output !== v.output || c.cacheWrite !== v.cacheWrite)
183
+ return false;
184
+ // cost consistency: recomputed from the components × the version's
185
+ // pinned table, at the record's own route (the route context lives
186
+ // in the record; the standalone schema cannot check this). A null
187
+ // costUsd is the R5b-④c absent stamp (the table has no rate for
188
+ // this route) — nothing to recompute against, accepted. The
189
+ // cross-check runs only for builtin-id records: a foreign table's
190
+ // consistency is the billing layer's accounting, not the ledger's.
191
+ if (c.costUsd !== null && c.pricingTableId === PRICING_TABLE_V1.id) {
192
+ const expected = priceFor(v.provider, { input: c.input, output: c.output, cacheRead: c.cacheRead, cacheWrite: c.cacheWrite }, pricingTableFor(c.pricingTableVersion));
193
+ // non-null for the builtin table by construction (both real
194
+ // routes + the mirror fallback) — a null here is a code bug,
195
+ // and an epsilon check has nothing to compare
196
+ if (expected !== null && Math.abs(c.costUsd - expected) > 1e-6)
197
+ return false;
198
+ }
199
+ }
200
+ if (!isNumber(v.ts))
201
+ return false;
202
+ return true;
203
+ }
204
+ export function validateTraceLine(v) {
205
+ if (!isRecord(v))
206
+ return false;
207
+ // both ledger generations are readable (R1d-1); the per-kind shapes are
208
+ // identical across 1 → 2 — only the request line's field set differs
209
+ // (v1 lacks the canonical block), handled by validateTraceRecord's
210
+ // version dispatch
211
+ if (!TRACE_SCHEMA_VERSIONS.has(v.schemaVersion))
212
+ return false;
213
+ switch (v.kind) {
214
+ case "header":
215
+ return (hasClosedKeys(v, ["schemaVersion", "kind", "sessionId", "kisoVersion", "createdAt"]) &&
216
+ typeof v.sessionId === "string" &&
217
+ typeof v.kisoVersion === "string" &&
218
+ isNumber(v.createdAt));
219
+ case "request":
220
+ return validateTraceRecord(v);
221
+ case "run_end":
222
+ return (hasClosedKeys(v, ["schemaVersion", "kind", "runId", "ts", "lastRequestIndex"]) &&
223
+ typeof v.runId === "string" &&
224
+ isNumber(v.ts) &&
225
+ // -1 = the run made no adapter calls (an empty run still
226
+ // settles cleanly); anything below that is not a request index
227
+ typeof v.lastRequestIndex === "number" &&
228
+ Number.isInteger(v.lastRequestIndex) &&
229
+ v.lastRequestIndex >= -1);
230
+ case "crash":
231
+ return (hasClosedKeys(v, ["schemaVersion", "kind", "ts", "note"]) &&
232
+ isNumber(v.ts) &&
233
+ typeof v.note === "string");
234
+ default:
235
+ return false;
236
+ }
237
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * E1 (1.2.0) — slice 2, the trace writer (proposal §1.2 + §6).
3
+ *
4
+ * The ledger is OUT-side (ADR-0051 §6): a versionable observation
5
+ * artifact, never part of the correctness ABI. The soft-fail law
6
+ * (work-order): trace loss costs ONE stderr line, then the session runs
7
+ * on — the writer degrades to a no-op on its first I/O failure and
8
+ * never throws into the caller.
9
+ *
10
+ * Async discipline (work-order §9): request lines are ENQUEUED (a sync
11
+ * push, sub-microsecond) and flushed on setImmediate — the model hot
12
+ * path never blocks on the ledger. The run_end line is flushed
13
+ * SYNCHRONOUSLY from the run's finally block: clean-settle marking is
14
+ * where the ledger's story (every hole reconciles as a crash, never as
15
+ * a writer bug) needs the write to have happened before the run ends.
16
+ */
17
+ import { type TraceLine } from "./record.js";
18
+ /** The runtime's own version, read from the package.json next to the
19
+ * build; null on failure (soft-fail — the header still ships). */
20
+ export declare function runtimeVersion(): string | null;
21
+ export interface TraceWriterDeps {
22
+ /** The sessions dir (store.root). The ledger lives in <root>/traces/ —
23
+ * NOT flat in root: a flat `<sid>.trace.jsonl` would be enumerated as
24
+ * a phantom session by SessionStore.list() and corrupt the bench
25
+ * sessions glob (proposal §3.2; the R3a gate pins it). */
26
+ root: string;
27
+ sessionId: string;
28
+ }
29
+ export declare class TraceWriter {
30
+ #private;
31
+ constructor(deps: TraceWriterDeps);
32
+ /** One-time setup: mkdir the ledger dir, mark a previous run that left
33
+ * no run_end (crash), write the header — the header once per ledger
34
+ * file (session-level, §1.2), so a resume appends markers, never a
35
+ * second header. Never throws. */
36
+ init(): void;
37
+ /** Enqueue a line for the next flush — the hot path's only cost.
38
+ * Never throws; after a degradation the line is dropped. */
39
+ enqueue(line: TraceLine): void;
40
+ /** Clean-settle marking: the run_end line lands NOW (synchronous),
41
+ * together with any still-pending request lines. */
42
+ finishRun(runId: string, lastRequestIndex: number): void;
43
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * E1 (1.2.0) — slice 2, the trace writer (proposal §1.2 + §6).
3
+ *
4
+ * The ledger is OUT-side (ADR-0051 §6): a versionable observation
5
+ * artifact, never part of the correctness ABI. The soft-fail law
6
+ * (work-order): trace loss costs ONE stderr line, then the session runs
7
+ * on — the writer degrades to a no-op on its first I/O failure and
8
+ * never throws into the caller.
9
+ *
10
+ * Async discipline (work-order §9): request lines are ENQUEUED (a sync
11
+ * push, sub-microsecond) and flushed on setImmediate — the model hot
12
+ * path never blocks on the ledger. The run_end line is flushed
13
+ * SYNCHRONOUSLY from the run's finally block: clean-settle marking is
14
+ * where the ledger's story (every hole reconciles as a crash, never as
15
+ * a writer bug) needs the write to have happened before the run ends.
16
+ */
17
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { TRACE_SCHEMA_VERSION } from "./record.js";
21
+ let cachedRuntimeVersion;
22
+ /** The runtime's own version, read from the package.json next to the
23
+ * build; null on failure (soft-fail — the header still ships). */
24
+ export function runtimeVersion() {
25
+ if (cachedRuntimeVersion === undefined) {
26
+ cachedRuntimeVersion = (() => {
27
+ try {
28
+ const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json"), "utf8"));
29
+ return pkg.version ?? null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ })();
35
+ }
36
+ return cachedRuntimeVersion;
37
+ }
38
+ export class TraceWriter {
39
+ #root;
40
+ #sessionId;
41
+ #path;
42
+ #pending = [];
43
+ #flushScheduled = false;
44
+ #degraded = false;
45
+ constructor(deps) {
46
+ this.#root = deps.root;
47
+ this.#sessionId = deps.sessionId;
48
+ this.#path = join(deps.root, "traces", `${deps.sessionId}.jsonl`);
49
+ }
50
+ /** One-time setup: mkdir the ledger dir, mark a previous run that left
51
+ * no run_end (crash), write the header — the header once per ledger
52
+ * file (session-level, §1.2), so a resume appends markers, never a
53
+ * second header. Never throws. */
54
+ init() {
55
+ if (this.#degraded)
56
+ return;
57
+ try {
58
+ mkdirSync(join(this.#root, "traces"), { recursive: true });
59
+ const fresh = !existsSync(this.#path);
60
+ if (this.#crashDetect()) {
61
+ this.#writeSync([
62
+ { schemaVersion: TRACE_SCHEMA_VERSION, kind: "crash", ts: Date.now(), note: "previous run left no run_end" },
63
+ ]);
64
+ }
65
+ if (fresh) {
66
+ this.#writeSync([
67
+ {
68
+ schemaVersion: TRACE_SCHEMA_VERSION,
69
+ kind: "header",
70
+ sessionId: this.#sessionId,
71
+ kisoVersion: runtimeVersion() ?? "?",
72
+ createdAt: Date.now(),
73
+ },
74
+ ]);
75
+ }
76
+ }
77
+ catch (err) {
78
+ this.#degrade(err);
79
+ }
80
+ }
81
+ /** Enqueue a line for the next flush — the hot path's only cost.
82
+ * Never throws; after a degradation the line is dropped. */
83
+ enqueue(line) {
84
+ if (this.#degraded)
85
+ return;
86
+ this.#pending.push(line);
87
+ if (!this.#flushScheduled) {
88
+ this.#flushScheduled = true;
89
+ setImmediate(() => {
90
+ this.#flushScheduled = false;
91
+ this.#flushSync();
92
+ });
93
+ }
94
+ }
95
+ /** Clean-settle marking: the run_end line lands NOW (synchronous),
96
+ * together with any still-pending request lines. */
97
+ finishRun(runId, lastRequestIndex) {
98
+ this.enqueue({
99
+ schemaVersion: TRACE_SCHEMA_VERSION,
100
+ kind: "run_end",
101
+ runId,
102
+ ts: Date.now(),
103
+ lastRequestIndex,
104
+ });
105
+ this.#flushSync();
106
+ }
107
+ /** True when the ledger already exists and its LAST line is neither
108
+ * run_end nor crash — an un-terminated run (killed mid-run), to be
109
+ * marked. A crash-marked ledger is not re-marked. */
110
+ #crashDetect() {
111
+ if (!existsSync(this.#path))
112
+ return false;
113
+ let last;
114
+ try {
115
+ for (const line of readFileSync(this.#path, "utf8").split("\n")) {
116
+ if (line.trim() === "")
117
+ continue;
118
+ last = JSON.parse(line);
119
+ }
120
+ }
121
+ catch {
122
+ return false; // unreadable ledger — do not compound the failure
123
+ }
124
+ if (last === undefined)
125
+ return false;
126
+ return last.kind !== "run_end" && last.kind !== "crash";
127
+ }
128
+ #writeSync(lines) {
129
+ if (lines.length === 0)
130
+ return;
131
+ try {
132
+ appendFileSync(this.#path, `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`);
133
+ }
134
+ catch (err) {
135
+ this.#degrade(err);
136
+ }
137
+ }
138
+ #flushSync() {
139
+ if (this.#pending.length === 0)
140
+ return;
141
+ const lines = this.#pending;
142
+ this.#pending = [];
143
+ this.#writeSync(lines);
144
+ }
145
+ /** The soft-fail law: one stderr line, then the ledger is dropped and
146
+ * the session runs on. Idempotent. */
147
+ #degrade(err) {
148
+ if (this.#degraded)
149
+ return;
150
+ this.#degraded = true;
151
+ this.#pending = [];
152
+ console.error(`[kiso] trace writer degraded (${err instanceof Error ? err.message : String(err)}); request traces dropped`);
153
+ }
154
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * E2 (1.3.0) — Canonical Usage: the reconciled usage schema (proposal §1
3
+ * as ruled 2026-08-13; R1a-R5 all adopted).
4
+ *
5
+ * THE PINNED SENTENCE (R1a): one canonical meaning for `input`, everywhere
6
+ * in kiso, forever: `input` is FRESH-ONLY — the tokens NOT served from
7
+ * cache. `total = input + cacheRead + cacheWrite` is the derived quantity,
8
+ * never a reported one. A cache ratio is always `cacheRead / total` — the
9
+ * numerator can never exceed the denominator. The >100% disease is
10
+ * structurally impossible under the canonical schema.
11
+ *
12
+ * The mapping keys on the ROUTE, not the provider (R2a): DeepSeek serves
13
+ * both routes and reports input fresh-only on one (anthropic-compat) and
14
+ * TOTAL on the other (openai-compat) — the SAME canonical meaning falls
15
+ * out of the route key (the dual-endpoint closure). The route string is
16
+ * the config adapter identity the guard already holds (`"anthropic" |
17
+ * "openai-compat"`).
18
+ *
19
+ * Cost comes from the versioned pricing table — every cost carries the
20
+ * table version it was computed with (R1c). The built-in v1 table prices
21
+ * both routes at DeepSeek's published rates; the caveat sentence from the
22
+ * bench README carries forward verbatim ("an approximation, not a bill").
23
+ * The commercial table is the product's Phase 10 (the table is injectable;
24
+ * the version travels with every cost).
25
+ *
26
+ * The frozen usage union stays provider-raw — this module derives at the
27
+ * accounting boundary (R4 Case B, pure derivation; the union does not
28
+ * move). Raw is provider observation; canonical is the accounting truth.
29
+ */
30
+ /** The union's usage shape (provider-raw — core protocol `events.Usage`). */
31
+ export interface RawUsage {
32
+ readonly inputTokens: number | null;
33
+ readonly outputTokens: number | null;
34
+ readonly cacheRead: number | null;
35
+ readonly cacheWrite: number | null;
36
+ }
37
+ /** The ONE canonical usage record. Every field has exactly one meaning
38
+ * (proposal §1.2). wall/ttft live in the trace record (latencyMs/ttftMs)
39
+ * — one source, never duplicated here. */
40
+ export interface CanonicalUsage {
41
+ /** FRESH-ONLY — the pinned sentence above. total = input + cacheRead +
42
+ * cacheWrite is the derived quantity, never a reported one. */
43
+ readonly input: number;
44
+ readonly output: number;
45
+ readonly cacheRead: number;
46
+ /** null = the provider reports none (openai-compat honestly does). */
47
+ readonly cacheWrite: number | null;
48
+ /** null = no reasoning split reported (reserved — every provider today). */
49
+ readonly reasoning: number | null;
50
+ /** USD from the pricing table below — null is the R5b-④c ABSENT stamp:
51
+ * the table has no rate for this route (an injected table's hole is
52
+ * explicit absent, never backfilled from the builtin table). The
53
+ * builtin v1 table covers both real routes, so the null branch is
54
+ * unreachable today — but the type must be able to express it.
55
+ * Never reported without its (pricingTableId, pricingTableVersion)
56
+ * tuple — the billing-basis ground. */
57
+ readonly costUsd: number | null;
58
+ /** The table the cost was computed with (R5b-④b): the (id, version)
59
+ * two-tuple stamp — cross-table version reconciliation and billing
60
+ * disputes answerable. "builtin" = the pinned in-repo table below;
61
+ * injected tables carry their own id. */
62
+ readonly pricingTableId: string;
63
+ readonly pricingTableVersion: number;
64
+ }
65
+ /** The versioned pricing table — a rate change is a version bump, never an
66
+ * edit; every cost records the version it was computed with (R1c). The
67
+ * id is the table's identity (R5b-④b): "builtin" for the pinned in-repo
68
+ * table, anything else for an injected commercial table. */
69
+ export interface PricingTable {
70
+ readonly id: string;
71
+ readonly version: number;
72
+ readonly entries: Readonly<Record<string, PricingEntry>>;
73
+ }
74
+ export interface PricingEntry {
75
+ /** USD per 1M FRESH input tokens. */
76
+ readonly inputPerM: number;
77
+ readonly outputPerM: number;
78
+ readonly cacheReadPerM: number;
79
+ /** USD per 1M cache-creation tokens. 0 = the provider does not price
80
+ * cache writes separately (DeepSeek's automatic caching). */
81
+ readonly cacheWritePerM: number;
82
+ }
83
+ /** The route → input-convention table (R2a). `input` semantics differ
84
+ * between the two routes; the convention is a property of the ROUTE, not
85
+ * the provider. Unknown routes fall back to "total" — exactly the
86
+ * incumbent guard behavior (the guard's else-branch: non-anthropic =
87
+ * total), preserved by construction. */
88
+ export declare const INPUT_CONVENTIONS: Readonly<Record<string, "fresh" | "total">>;
89
+ /** Pricing table v1. Freeze date 2026-08-13 (the E2 ruling). Rates:
90
+ * DeepSeek's published rates (https://api-docs.deepseek.com/quick_start/pricing),
91
+ * the 0.1 cache-hit ratio the bench has used since the 0.1.23 round. The
92
+ * caveat sentence carries forward verbatim (bench README): "an
93
+ * approximation, not a bill." */
94
+ export declare const PRICING_TABLE_V1: PricingTable;
95
+ export declare const PRICING_TABLES: Readonly<Record<number, PricingTable>>;
96
+ /** The table pinned for a version; throws when none is pinned (a cost
97
+ * recorded against an unpinned version is a ledger corruption). */
98
+ export declare function pricingTableFor(version: number): PricingTable;
99
+ export declare function priceFor(route: string, u: {
100
+ input: number;
101
+ output: number;
102
+ cacheRead: number;
103
+ cacheWrite: number | null;
104
+ }, table: PricingTable): number | null;
105
+ /** Raw → canonical (the accounting boundary). Behavior-preserving against
106
+ * the guard's incumbent per-provider branch (guard.ts settle): anthropic
107
+ * input is fresh as-is; every other route subtracts cacheRead.
108
+ *
109
+ * The trailing `table` parameter is the injection slot (R5b-④a): the
110
+ * R5a-1-commercial table rides it on day one, defaulting to the pinned
111
+ * builtin v1 table so the export's default behavior is the pinned one
112
+ * and a future injection never widens this frozen signature. */
113
+ export declare function canonicalizeUsage(route: string, raw: RawUsage, table?: PricingTable): CanonicalUsage;
114
+ /** The canonical schema's invariants, machine-checked: closed key set;
115
+ * non-negative numbers; cacheWrite/reasoning null-or-number; the pinned
116
+ * sentence — cacheRead can never exceed total (= input + cacheRead +
117
+ * cacheWrite) — structurally true for non-negatives but pinned against
118
+ * future edits; costUsd null-or-number (R5b-④c — null is the injected
119
+ * table's absent stamp); the (pricingTableId, pricingTableVersion)
120
+ * tuple, with the registry pin scoped to the builtin id (R5b-④b) — an
121
+ * injected table's version is the injector's accounting, the ledger
122
+ * cannot know tables it does not carry. */
123
+ export declare function validateCanonicalUsage(v: unknown): v is CanonicalUsage;