@vincemakes/kiso-runtime 0.36.0 → 0.37.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/dist/provider/metadata.js +66 -3
- package/dist/session.d.ts +23 -0
- package/dist/session.js +31 -1
- package/dist/summarize.js +13 -1
- package/dist/trace/guard.js +48 -0
- package/dist/trace/record.d.ts +78 -8
- package/dist/trace/record.js +91 -7
- package/dist/usage/canonical.d.ts +15 -1
- package/dist/usage/canonical.js +5 -1
- package/package.json +6 -6
|
@@ -189,7 +189,28 @@ const ENTRIES = [
|
|
|
189
189
|
capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: {
|
|
190
190
|
emitsThinkingStream: true,
|
|
191
191
|
thinking: { modes: ["enabled", "disabled"], default: "enabled" },
|
|
192
|
-
|
|
192
|
+
// `none` added 2026-09-14 from a live probe. The three natives were
|
|
193
|
+
// RIGHT and stay: the endpoint's enum accepts seven values, but
|
|
194
|
+
// the vendor maps minimal->low, medium->high and xhigh->high, so
|
|
195
|
+
// those three are the provider's own aliases and this row's rule
|
|
196
|
+
// is that an alias is never shown as native. Offering `medium`
|
|
197
|
+
// would promise a level the model does not have.
|
|
198
|
+
//
|
|
199
|
+
// `none` is not an alias. It turns thinking OFF: the response
|
|
200
|
+
// carries no reasoning_content and completion_tokens_details is
|
|
201
|
+
// null outright. A distinct behaviour the row simply lacked, and
|
|
202
|
+
// the only reachable way to buy no reasoning at all.
|
|
203
|
+
//
|
|
204
|
+
// Measured, 12 runs per level on one task (median reasoning
|
|
205
|
+
// tokens): minimal 191, low 186 | medium 212, high 199, xhigh 195
|
|
206
|
+
// | max 229. The grouping and the ordering match the vendor's
|
|
207
|
+
// mapping. The unset default sits at 219, which this sample
|
|
208
|
+
// cannot separate from high or from max, so `default` stays what
|
|
209
|
+
// the vendor documents rather than what one task suggests.
|
|
210
|
+
//
|
|
211
|
+
// bench/probe-effort-levels.mjs reads the accepted set from the
|
|
212
|
+
// endpoint's own refusal and costs nothing.
|
|
213
|
+
effort: { levels: ["none", "low", "high", "max"], default: "high", wire: "reasoning_effort" },
|
|
193
214
|
asOf: "2026-08-26",
|
|
194
215
|
source: "https://api-docs.deepseek.com/guides/thinking_mode",
|
|
195
216
|
}, inputModalities: null },
|
|
@@ -223,7 +244,28 @@ const ENTRIES = [
|
|
|
223
244
|
capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: {
|
|
224
245
|
emitsThinkingStream: true,
|
|
225
246
|
thinking: { modes: ["enabled", "disabled"], default: "enabled" },
|
|
226
|
-
|
|
247
|
+
// `none` added 2026-09-14 from a live probe. The three natives were
|
|
248
|
+
// RIGHT and stay: the endpoint's enum accepts seven values, but
|
|
249
|
+
// the vendor maps minimal->low, medium->high and xhigh->high, so
|
|
250
|
+
// those three are the provider's own aliases and this row's rule
|
|
251
|
+
// is that an alias is never shown as native. Offering `medium`
|
|
252
|
+
// would promise a level the model does not have.
|
|
253
|
+
//
|
|
254
|
+
// `none` is not an alias. It turns thinking OFF: the response
|
|
255
|
+
// carries no reasoning_content and completion_tokens_details is
|
|
256
|
+
// null outright. A distinct behaviour the row simply lacked, and
|
|
257
|
+
// the only reachable way to buy no reasoning at all.
|
|
258
|
+
//
|
|
259
|
+
// Measured, 12 runs per level on one task (median reasoning
|
|
260
|
+
// tokens): minimal 191, low 186 | medium 212, high 199, xhigh 195
|
|
261
|
+
// | max 229. The grouping and the ordering match the vendor's
|
|
262
|
+
// mapping. The unset default sits at 219, which this sample
|
|
263
|
+
// cannot separate from high or from max, so `default` stays what
|
|
264
|
+
// the vendor documents rather than what one task suggests.
|
|
265
|
+
//
|
|
266
|
+
// bench/probe-effort-levels.mjs reads the accepted set from the
|
|
267
|
+
// endpoint's own refusal and costs nothing.
|
|
268
|
+
effort: { levels: ["none", "low", "high", "max"], default: "high", wire: "reasoning_effort" },
|
|
227
269
|
asOf: "2026-09-12",
|
|
228
270
|
source: "https://api-docs.deepseek.com/quick_start/pricing/",
|
|
229
271
|
}, inputModalities: null },
|
|
@@ -238,7 +280,28 @@ const ENTRIES = [
|
|
|
238
280
|
capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: {
|
|
239
281
|
emitsThinkingStream: true,
|
|
240
282
|
thinking: { modes: ["enabled", "disabled"], default: "enabled" },
|
|
241
|
-
|
|
283
|
+
// `none` added 2026-09-14 from a live probe. The three natives were
|
|
284
|
+
// RIGHT and stay: the endpoint's enum accepts seven values, but
|
|
285
|
+
// the vendor maps minimal->low, medium->high and xhigh->high, so
|
|
286
|
+
// those three are the provider's own aliases and this row's rule
|
|
287
|
+
// is that an alias is never shown as native. Offering `medium`
|
|
288
|
+
// would promise a level the model does not have.
|
|
289
|
+
//
|
|
290
|
+
// `none` is not an alias. It turns thinking OFF: the response
|
|
291
|
+
// carries no reasoning_content and completion_tokens_details is
|
|
292
|
+
// null outright. A distinct behaviour the row simply lacked, and
|
|
293
|
+
// the only reachable way to buy no reasoning at all.
|
|
294
|
+
//
|
|
295
|
+
// Measured, 12 runs per level on one task (median reasoning
|
|
296
|
+
// tokens): minimal 191, low 186 | medium 212, high 199, xhigh 195
|
|
297
|
+
// | max 229. The grouping and the ordering match the vendor's
|
|
298
|
+
// mapping. The unset default sits at 219, which this sample
|
|
299
|
+
// cannot separate from high or from max, so `default` stays what
|
|
300
|
+
// the vendor documents rather than what one task suggests.
|
|
301
|
+
//
|
|
302
|
+
// bench/probe-effort-levels.mjs reads the accepted set from the
|
|
303
|
+
// endpoint's own refusal and costs nothing.
|
|
304
|
+
effort: { levels: ["none", "low", "high", "max"], default: "high", wire: "reasoning_effort" },
|
|
242
305
|
asOf: "2026-08-26",
|
|
243
306
|
source: "https://api-docs.deepseek.com/guides/thinking_mode",
|
|
244
307
|
}, inputModalities: null },
|
package/dist/session.d.ts
CHANGED
|
@@ -141,7 +141,30 @@ export declare class AgentSession {
|
|
|
141
141
|
/** XP-1: the reasoning axes travel with the binding too; absent =
|
|
142
142
|
* fresh defaults (a new binding never inherits stale effort). */
|
|
143
143
|
readonly reasoning?: ReasoningSetting;
|
|
144
|
+
/** CTX-1: the compaction threshold follows the live model's window.
|
|
145
|
+
* Absent KEEPS the current one — a caller that does not know the new
|
|
146
|
+
* model's window must not silently reset the policy to nothing. */
|
|
147
|
+
readonly microcompact?: {
|
|
148
|
+
readonly thresholdTokens: number;
|
|
149
|
+
};
|
|
144
150
|
}): void;
|
|
151
|
+
/**
|
|
152
|
+
* CTX-1: the compaction threshold alone, with NO profile revision.
|
|
153
|
+
*
|
|
154
|
+
* `setModelBinding` is the atomic switch — the adapter and everything
|
|
155
|
+
* that must travel with it, recorded durably. This is the other door:
|
|
156
|
+
* a session OPENED onto a model the process was not configured for.
|
|
157
|
+
* `/resume` restores the recorded model, which can be any model the
|
|
158
|
+
* session ever used, while the threshold came from whatever model this
|
|
159
|
+
* process started on. Resuming a 1M session from a 200k start left it
|
|
160
|
+
* clearing tool results at 100,000.
|
|
161
|
+
*
|
|
162
|
+
* It writes no revision because nothing was selected: the caller is
|
|
163
|
+
* bringing the policy into line with a model already in force, not
|
|
164
|
+
* choosing one. Recording a revision here would write a new profile on
|
|
165
|
+
* every open.
|
|
166
|
+
*/
|
|
167
|
+
setMicrocompactThreshold(thresholdTokens: number): void;
|
|
145
168
|
/** E2: the adapter identity (anthropic / openai-compat / openai-responses) — the route
|
|
146
169
|
* key the canonical consumer (CLI usage, the trace block) keys on. The
|
|
147
170
|
* per-run tracer reads the SAME live binding; one source, one
|
package/dist/session.js
CHANGED
|
@@ -92,6 +92,12 @@ export class AgentSession {
|
|
|
92
92
|
#continuationScope;
|
|
93
93
|
// XP-1: the selected axes; resolved per request (next-turn semantics).
|
|
94
94
|
#reasoning;
|
|
95
|
+
// CTX-1: the compaction threshold is derived from the LIVE model's window,
|
|
96
|
+
// so it is a passenger on the binding like the four above. PH-F8 fixed
|
|
97
|
+
// exactly this class — a frozen startup value still in use after a switch
|
|
98
|
+
// — for the model id, the provider, the endpoint and the scope. The
|
|
99
|
+
// threshold was one field over and was never asked.
|
|
100
|
+
#microcompact;
|
|
95
101
|
// XP-1: a legacy session records revision 1 at the next explicit
|
|
96
102
|
// selection or first request — never eagerly at open.
|
|
97
103
|
#profilePending;
|
|
@@ -146,6 +152,8 @@ export class AgentSession {
|
|
|
146
152
|
this.#baseUrl = config.baseUrl;
|
|
147
153
|
this.#continuationScope = config.continuationScope;
|
|
148
154
|
this.#reasoning = config.reasoning ?? { thinking: "default", effort: "default" };
|
|
155
|
+
// CTX-1: starts as the startup policy, then follows the binding.
|
|
156
|
+
this.#microcompact = config.microcompact;
|
|
149
157
|
this.#profilePending = config.profilePending === true;
|
|
150
158
|
}
|
|
151
159
|
/** The config a NEW run/resume/summary sees: the frozen startup config
|
|
@@ -153,9 +161,10 @@ export class AgentSession {
|
|
|
153
161
|
* fresh per call so an in-flight run keeps the config it started with
|
|
154
162
|
* — the same boundary setAdapter has always drawn. */
|
|
155
163
|
#effectiveConfig() {
|
|
156
|
-
const { provider: _startup, baseUrl: _startupUrl, continuationScope: _startupScope, ...rest } = this.#config;
|
|
164
|
+
const { provider: _startup, baseUrl: _startupUrl, continuationScope: _startupScope, microcompact: _startupMicro, ...rest } = this.#config;
|
|
157
165
|
return {
|
|
158
166
|
...rest,
|
|
167
|
+
...(this.#microcompact !== undefined ? { microcompact: this.#microcompact } : {}),
|
|
159
168
|
model: this.#model,
|
|
160
169
|
...(this.#provider !== undefined ? { provider: this.#provider } : {}),
|
|
161
170
|
...(this.#baseUrl !== undefined ? { baseUrl: this.#baseUrl } : {}),
|
|
@@ -224,10 +233,31 @@ export class AgentSession {
|
|
|
224
233
|
this.#baseUrl = binding.baseUrl;
|
|
225
234
|
this.#continuationScope = binding.scope;
|
|
226
235
|
this.#reasoning = binding.reasoning ?? { thinking: "default", effort: "default" };
|
|
236
|
+
if (binding.microcompact !== undefined)
|
|
237
|
+
this.#microcompact = binding.microcompact;
|
|
227
238
|
// XP-1: an explicit selection is DURABLE — the setting survives
|
|
228
239
|
// /resume because a revision records it now, not at some later flush.
|
|
229
240
|
this.#recordProfile();
|
|
230
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* CTX-1: the compaction threshold alone, with NO profile revision.
|
|
244
|
+
*
|
|
245
|
+
* `setModelBinding` is the atomic switch — the adapter and everything
|
|
246
|
+
* that must travel with it, recorded durably. This is the other door:
|
|
247
|
+
* a session OPENED onto a model the process was not configured for.
|
|
248
|
+
* `/resume` restores the recorded model, which can be any model the
|
|
249
|
+
* session ever used, while the threshold came from whatever model this
|
|
250
|
+
* process started on. Resuming a 1M session from a 200k start left it
|
|
251
|
+
* clearing tool results at 100,000.
|
|
252
|
+
*
|
|
253
|
+
* It writes no revision because nothing was selected: the caller is
|
|
254
|
+
* bringing the policy into line with a model already in force, not
|
|
255
|
+
* choosing one. Recording a revision here would write a new profile on
|
|
256
|
+
* every open.
|
|
257
|
+
*/
|
|
258
|
+
setMicrocompactThreshold(thresholdTokens) {
|
|
259
|
+
this.#microcompact = { thresholdTokens };
|
|
260
|
+
}
|
|
231
261
|
/** E2: the adapter identity (anthropic / openai-compat / openai-responses) — the route
|
|
232
262
|
* key the canonical consumer (CLI usage, the trace block) keys on. The
|
|
233
263
|
* per-run tracer reads the SAME live binding; one source, one
|
package/dist/summarize.js
CHANGED
|
@@ -226,7 +226,19 @@ export async function summarizeConversation(options) {
|
|
|
226
226
|
}
|
|
227
227
|
// The LAST usage event is the call's (a turn reports usage once).
|
|
228
228
|
if (ev.type === "usage" && ev.known) {
|
|
229
|
-
|
|
229
|
+
// RSN-1, and the summary path was missed on the first wiring:
|
|
230
|
+
// the quartet was copied field by field, so a fifth field added
|
|
231
|
+
// later is silently dropped. The summary IS a billed call whose
|
|
232
|
+
// cost the ledger records; leaving its split behind made that
|
|
233
|
+
// one call's thinking unknown while every other call's was
|
|
234
|
+
// measured. The output total was never wrong — the split was.
|
|
235
|
+
usage = {
|
|
236
|
+
inputTokens: ev.inputTokens,
|
|
237
|
+
outputTokens: ev.outputTokens,
|
|
238
|
+
cacheRead: ev.cacheRead,
|
|
239
|
+
cacheWrite: ev.cacheWrite,
|
|
240
|
+
...(ev.reasoningTokens !== undefined ? { reasoningTokens: ev.reasoningTokens } : {}),
|
|
241
|
+
};
|
|
230
242
|
}
|
|
231
243
|
}
|
|
232
244
|
if (stops === 0)
|
package/dist/trace/guard.js
CHANGED
|
@@ -71,6 +71,14 @@ export class RequestTracer {
|
|
|
71
71
|
let cacheWrite = null;
|
|
72
72
|
let outputTokens = null;
|
|
73
73
|
let usageKnown = false;
|
|
74
|
+
// TRACE-F1: the server's own statement of what it ran. Null until a
|
|
75
|
+
// usage event carries one; it stays null when the provider says
|
|
76
|
+
// nothing, which is a different fact from "it matched".
|
|
77
|
+
let servedModel = null;
|
|
78
|
+
// RSN-1: the reported thinking split. Undefined until a usage event
|
|
79
|
+
// carries one; it stays undefined when the provider reports none,
|
|
80
|
+
// which is a different fact from a measured zero.
|
|
81
|
+
let reasoningTokens;
|
|
74
82
|
const toolCalls = [];
|
|
75
83
|
let outcome = "ok";
|
|
76
84
|
try {
|
|
@@ -91,6 +99,10 @@ export class RequestTracer {
|
|
|
91
99
|
cacheWrite = ev.cacheWrite;
|
|
92
100
|
if (ev.outputTokens !== null)
|
|
93
101
|
outputTokens = ev.outputTokens;
|
|
102
|
+
if (ev.servedModel !== undefined)
|
|
103
|
+
servedModel = ev.servedModel;
|
|
104
|
+
if (ev.reasoningTokens !== undefined)
|
|
105
|
+
reasoningTokens = ev.reasoningTokens;
|
|
94
106
|
}
|
|
95
107
|
yield ev;
|
|
96
108
|
}
|
|
@@ -111,6 +123,8 @@ export class RequestTracer {
|
|
|
111
123
|
cacheWrite,
|
|
112
124
|
outputTokens,
|
|
113
125
|
usageKnown,
|
|
126
|
+
servedModel,
|
|
127
|
+
reasoningTokens,
|
|
114
128
|
});
|
|
115
129
|
}
|
|
116
130
|
}
|
|
@@ -163,6 +177,10 @@ export class RequestTracer {
|
|
|
163
177
|
// turn (freshness fresh) is never part of it (slice 4)
|
|
164
178
|
segmentHashes: hashes,
|
|
165
179
|
stablePrefixFingerprint: stablePrefixFingerprint(cacheableHashes(manifest, hashes)),
|
|
180
|
+
// F33-1: the convention below writes unknown as zero, so the
|
|
181
|
+
// record says out loud which of the two it is. False until a
|
|
182
|
+
// usage event settles it.
|
|
183
|
+
usageKnown: false,
|
|
166
184
|
freshInput: 0, // unknown until the usage event — "0 = unknown"
|
|
167
185
|
cacheRead: 0,
|
|
168
186
|
cacheWrite: null,
|
|
@@ -192,6 +210,35 @@ export class RequestTracer {
|
|
|
192
210
|
record.latencyMs = performance.now() - p.t0;
|
|
193
211
|
record.ttftMs = p.ttftMs ?? 0; // null = no event ever — the "0 = unknown" marker
|
|
194
212
|
record.toolCalls = p.toolCalls;
|
|
213
|
+
// F33-1: the settle path has always been TOLD whether the provider
|
|
214
|
+
// reported usage; it just never wrote it down, and every consumer
|
|
215
|
+
// downstream then had to guess from four zeros.
|
|
216
|
+
//
|
|
217
|
+
// F33-R3: but `known` is NOT completeness. Core states the contract
|
|
218
|
+
// exactly — `known: true` means AT LEAST ONE field was reported and
|
|
219
|
+
// the others may still be null (protocol/events.ts, Area 6 round 9)
|
|
220
|
+
// — and canonicalization then fills the unreported ones with zero.
|
|
221
|
+
// Copying the boolean therefore re-lost the distinction one level
|
|
222
|
+
// down: a provider that reported input and cache but no output wrote
|
|
223
|
+
// `usageKnown: true, canonical.output: 0`, and every consumer read a
|
|
224
|
+
// measured zero.
|
|
225
|
+
//
|
|
226
|
+
// What a consumer needs is whether THE FIELDS THE COST IS MADE OF
|
|
227
|
+
// were each reported. That is a property of the raw quartet, so it
|
|
228
|
+
// is computed from the raw quartet rather than inherited from a flag
|
|
229
|
+
// that answers a weaker question. A genuine reported zero stays
|
|
230
|
+
// known: null is unreported, 0 is measured, and only null makes this
|
|
231
|
+
// false.
|
|
232
|
+
record.usageKnown =
|
|
233
|
+
p.usageKnown &&
|
|
234
|
+
p.inputTokens !== null &&
|
|
235
|
+
p.cacheRead !== null &&
|
|
236
|
+
p.outputTokens !== null;
|
|
237
|
+
// TRACE-F1: written ONLY when the server stated one. The field is
|
|
238
|
+
// absent otherwise — writing the requested id here would manufacture
|
|
239
|
+
// the agreement the reconciliation exists to test.
|
|
240
|
+
if (p.servedModel !== null)
|
|
241
|
+
record.servedModel = p.servedModel;
|
|
195
242
|
if (p.usageKnown) {
|
|
196
243
|
record.freshInput =
|
|
197
244
|
this.#provider === "anthropic"
|
|
@@ -218,6 +265,7 @@ export class RequestTracer {
|
|
|
218
265
|
outputTokens: p.outputTokens,
|
|
219
266
|
cacheRead: p.cacheRead,
|
|
220
267
|
cacheWrite: p.cacheWrite,
|
|
268
|
+
...(p.reasoningTokens !== undefined ? { reasoningTokens: p.reasoningTokens } : {}),
|
|
221
269
|
});
|
|
222
270
|
this.#writer.enqueue(record);
|
|
223
271
|
}
|
package/dist/trace/record.d.ts
CHANGED
|
@@ -35,15 +35,64 @@
|
|
|
35
35
|
* FOR ("safer-options"). Optional by construction: adding a required
|
|
36
36
|
* field would have invalidated every v3 record ever written, and the
|
|
37
37
|
* whole generation-compat discipline exists to prevent exactly that.
|
|
38
|
+
*
|
|
39
|
+
* Astra F33-1 (0.36.x) — schemaVersion 5: the record gains `usageKnown`.
|
|
40
|
+
* The writer has always initialised the quartet to zero under an explicit
|
|
41
|
+
* "0 = unknown" convention, and settled it only when the provider actually
|
|
42
|
+
* reported usage — so a request nobody measured was written as a request
|
|
43
|
+
* that cost nothing, indistinguishable from a genuine zero. Every consumer
|
|
44
|
+
* reading the sidecar inherited that: the bench extractors' "unknown is not
|
|
45
|
+
* zero" fix ran only on the plain-log branch, and adding a conforming
|
|
46
|
+
* sidecar to a session with one unmeasured request flipped
|
|
47
|
+
* usage_incomplete from true to false.
|
|
48
|
+
*
|
|
49
|
+
* The settle path already receives `usageKnown`; it simply never wrote it
|
|
50
|
+
* down. Recording it makes the ledger self-describing, which is the only
|
|
51
|
+
* form in which a later reader can tell the two zeros apart. It is listed
|
|
52
|
+
* as OPTIONAL so that v1-v4 sidecars stay readable — their absence means
|
|
53
|
+
* "this generation could not say", which a consumer must treat as unknown
|
|
54
|
+
* rather than as complete.
|
|
55
|
+
*
|
|
56
|
+
* TRACE-F1 (0.36.x) — schemaVersion 6: the record gains `servedModel`, the
|
|
57
|
+
* id the SERVER says it served. Every generation before this one recorded
|
|
58
|
+
* only `model`, the id we asked for, and the two were assumed identical
|
|
59
|
+
* because nothing ever checked. A vendor is free to disagree: a retired
|
|
60
|
+
* name becomes an alias, a migration id resolves elsewhere, a tier is
|
|
61
|
+
* silently upgraded. The bench asked for `deepseek-v4-flash` on every leg
|
|
62
|
+
* for four days while the server answered as `deepseek-flash`, and the
|
|
63
|
+
* specified-vs-observed reconciliation built to catch precisely that
|
|
64
|
+
* reported nothing — it had no observed half to compare, so it compared
|
|
65
|
+
* the specification against itself.
|
|
66
|
+
*
|
|
67
|
+
* OPTIONAL, and absent means "the server stated nothing" — NEVER "the same
|
|
68
|
+
* as requested". That default is the whole defect: a field filled in with
|
|
69
|
+
* the request manufactures the agreement the reconciliation exists to
|
|
70
|
+
* test.
|
|
38
71
|
*/
|
|
39
|
-
/** schemaVersion:
|
|
72
|
+
/** schemaVersion: 6 for the `servedModel` statement (TRACE-F1);
|
|
73
|
+
* 5 was the `usageKnown` marker (F33-1). Version
|
|
40
74
|
* 1 = the 1.2.0 shape, 2 = the 1.3.0 shape, 3 = the 0.2.1 shape; all
|
|
41
75
|
* kept for generation-compat reads (R1d-1, R2-1). Algorithm and shape
|
|
42
76
|
* changes bump it (ADR-0051 §6 OUT-side versioning). */
|
|
43
|
-
export declare const TRACE_SCHEMA_VERSION =
|
|
77
|
+
export declare const TRACE_SCHEMA_VERSION = 6;
|
|
44
78
|
/** The versions a reader may meet in a ledger. v1 and v2 records are
|
|
45
79
|
* accepted (generation-compat) and read as defaults — no canonical
|
|
46
80
|
* block (v1), no rent block (v1, v2). */
|
|
81
|
+
/**
|
|
82
|
+
* EVERY generation ever written, named explicitly.
|
|
83
|
+
*
|
|
84
|
+
* F33-R1: this was `[1, 2, 3, TRACE_SCHEMA_VERSION]`, and a set spelled
|
|
85
|
+
* with a MOVING member drops the generation it was standing on every time
|
|
86
|
+
* the version rises. Raising 4 to 5 silently removed 4 — the version every
|
|
87
|
+
* trace the deployed 0.36.0 has ever written carries — so the reader
|
|
88
|
+
* stopped accepting the product's own live output while a v5 record went
|
|
89
|
+
* through. The whole generation-compat discipline exists to prevent that,
|
|
90
|
+
* and its own constant undid it.
|
|
91
|
+
*
|
|
92
|
+
* A new version is added here BY HAND, next to its field set and its hash
|
|
93
|
+
* spec. Three lists to extend is the point: a bump that forgets one is a
|
|
94
|
+
* bump that fails loudly rather than a reader that quietly narrows.
|
|
95
|
+
*/
|
|
47
96
|
export declare const TRACE_SCHEMA_VERSIONS: Readonly<Set<number>>;
|
|
48
97
|
import type { CanonicalUsage } from "../usage/canonical.js";
|
|
49
98
|
import { type RentLine } from "./rent.js";
|
|
@@ -62,7 +111,7 @@ export interface TraceSegment {
|
|
|
62
111
|
}
|
|
63
112
|
/** That is the complete set for 1.2.0. */
|
|
64
113
|
export interface TraceRecord {
|
|
65
|
-
schemaVersion:
|
|
114
|
+
schemaVersion: 6;
|
|
66
115
|
kind: "request";
|
|
67
116
|
requestId: string;
|
|
68
117
|
runId: string;
|
|
@@ -86,6 +135,12 @@ export interface TraceRecord {
|
|
|
86
135
|
* Canonical/billing usage lives in the `canonical` block (E2); these
|
|
87
136
|
* fields are observation only (a provider may count a token a dozen
|
|
88
137
|
* ways; billing must not). */
|
|
138
|
+
/** F33-1 (v5): did the provider actually REPORT usage for this request?
|
|
139
|
+
* The quartet below is written under a "0 = unknown" convention, so
|
|
140
|
+
* without this a request nobody measured and a request that genuinely
|
|
141
|
+
* cost nothing are the same four zeros. Optional only for reading v1-v4
|
|
142
|
+
* sidecars; every v5 writer records it. */
|
|
143
|
+
usageKnown?: boolean;
|
|
89
144
|
freshInput: number;
|
|
90
145
|
cacheRead: number;
|
|
91
146
|
cacheWrite: number | null;
|
|
@@ -117,25 +172,31 @@ export interface TraceRecord {
|
|
|
117
172
|
* its purpose: "safer-options". A consumer separating on-demand rent
|
|
118
173
|
* from run rent reads this field and needs no heuristic. */
|
|
119
174
|
purpose?: string;
|
|
175
|
+
/** TRACE-F1 (v6) — the model id the SERVER said it served, when it said
|
|
176
|
+
* one. ABSENT means the server stated nothing, NEVER "the same as
|
|
177
|
+
* requested": that default is what let a retired id read as live for
|
|
178
|
+
* four days. The requested id stays in `model`; these are two facts,
|
|
179
|
+
* never merged. */
|
|
180
|
+
servedModel?: string;
|
|
120
181
|
ts: number;
|
|
121
182
|
}
|
|
122
183
|
/** That is the complete set for 1.2.0. */
|
|
123
184
|
export interface HeaderLine {
|
|
124
|
-
schemaVersion:
|
|
185
|
+
schemaVersion: 6;
|
|
125
186
|
kind: "header";
|
|
126
187
|
sessionId: string;
|
|
127
188
|
kisoVersion: string;
|
|
128
189
|
createdAt: number;
|
|
129
190
|
}
|
|
130
191
|
export interface RunEndLine {
|
|
131
|
-
schemaVersion:
|
|
192
|
+
schemaVersion: 6;
|
|
132
193
|
kind: "run_end";
|
|
133
194
|
runId: string;
|
|
134
195
|
ts: number;
|
|
135
196
|
lastRequestIndex: number;
|
|
136
197
|
}
|
|
137
198
|
export interface CrashLine {
|
|
138
|
-
schemaVersion:
|
|
199
|
+
schemaVersion: 6;
|
|
139
200
|
kind: "crash";
|
|
140
201
|
ts: number;
|
|
141
202
|
note: string;
|
|
@@ -159,9 +220,18 @@ export declare const TRACE_RECORD_FIELDS_V3: readonly ["schemaVersion", "kind",
|
|
|
159
220
|
* TUI2-R3v2 ③: `purpose` is OPTIONAL — a run request omits it entirely,
|
|
160
221
|
* a side query names what it was for. It is listed in TRACE_RECORD_OPTIONAL
|
|
161
222
|
* below so the closed-set gate accepts a record without it. */
|
|
162
|
-
export declare const
|
|
223
|
+
export declare const TRACE_RECORD_FIELDS_V4: readonly ["schemaVersion", "kind", "requestId", "runId", "requestIndex", "retryAttempt", "provider", "model", "adapterVersion", "systemPromptHash", "toolSchemaHash", "contextHash", "contextManifest", "segmentHashes", "stablePrefixFingerprint", "freshInput", "cacheRead", "cacheWrite", "output", "latencyMs", "ttftMs", "toolCalls", "outcome", "lineageLink", "ts", "canonical", "rent", "purpose"];
|
|
224
|
+
/** The field set (schemaVersion 5) = the v4 set + `usageKnown` (F33-1).
|
|
225
|
+
* Optional for the same reason `purpose` is: requiring it would invalidate
|
|
226
|
+
* every v4 record ever written, and reading old generations is the whole
|
|
227
|
+
* point of the discipline. Absent means the generation could not say. */
|
|
228
|
+
export declare const TRACE_RECORD_FIELDS_V5: readonly ["schemaVersion", "kind", "requestId", "runId", "requestIndex", "retryAttempt", "provider", "model", "adapterVersion", "systemPromptHash", "toolSchemaHash", "contextHash", "contextManifest", "segmentHashes", "stablePrefixFingerprint", "freshInput", "cacheRead", "cacheWrite", "output", "latencyMs", "ttftMs", "toolCalls", "outcome", "lineageLink", "ts", "canonical", "rent", "purpose", "usageKnown"];
|
|
229
|
+
/** TRACE-F1 (v6): the model the SERVER said it served. A v5 ledger has none
|
|
230
|
+
* on any record, which reads as "nobody asked the server" — the true
|
|
231
|
+
* statement about a ledger written before the adapters read the field. */
|
|
232
|
+
export declare const TRACE_RECORD_FIELDS: readonly ["schemaVersion", "kind", "requestId", "runId", "requestIndex", "retryAttempt", "provider", "model", "adapterVersion", "systemPromptHash", "toolSchemaHash", "contextHash", "contextManifest", "segmentHashes", "stablePrefixFingerprint", "freshInput", "cacheRead", "cacheWrite", "output", "latencyMs", "ttftMs", "toolCalls", "outcome", "lineageLink", "ts", "canonical", "rent", "purpose", "usageKnown", "servedModel"];
|
|
163
233
|
/** The fields the closed-set check does not require to be present. */
|
|
164
|
-
export declare const TRACE_RECORD_OPTIONAL: readonly ["purpose"];
|
|
234
|
+
export declare const TRACE_RECORD_OPTIONAL: readonly ["purpose", "usageKnown", "servedModel"];
|
|
165
235
|
export declare const TRACE_SEGMENT_FIELDS: readonly ["role", "seqRange", "estTokens", "freshness"];
|
|
166
236
|
export declare function validateTraceSegment(v: unknown): v is TraceSegment;
|
|
167
237
|
export declare function validateTraceRecord(v: unknown): v is TraceRecord;
|
package/dist/trace/record.js
CHANGED
|
@@ -35,16 +35,65 @@
|
|
|
35
35
|
* FOR ("safer-options"). Optional by construction: adding a required
|
|
36
36
|
* field would have invalidated every v3 record ever written, and the
|
|
37
37
|
* whole generation-compat discipline exists to prevent exactly that.
|
|
38
|
+
*
|
|
39
|
+
* Astra F33-1 (0.36.x) — schemaVersion 5: the record gains `usageKnown`.
|
|
40
|
+
* The writer has always initialised the quartet to zero under an explicit
|
|
41
|
+
* "0 = unknown" convention, and settled it only when the provider actually
|
|
42
|
+
* reported usage — so a request nobody measured was written as a request
|
|
43
|
+
* that cost nothing, indistinguishable from a genuine zero. Every consumer
|
|
44
|
+
* reading the sidecar inherited that: the bench extractors' "unknown is not
|
|
45
|
+
* zero" fix ran only on the plain-log branch, and adding a conforming
|
|
46
|
+
* sidecar to a session with one unmeasured request flipped
|
|
47
|
+
* usage_incomplete from true to false.
|
|
48
|
+
*
|
|
49
|
+
* The settle path already receives `usageKnown`; it simply never wrote it
|
|
50
|
+
* down. Recording it makes the ledger self-describing, which is the only
|
|
51
|
+
* form in which a later reader can tell the two zeros apart. It is listed
|
|
52
|
+
* as OPTIONAL so that v1-v4 sidecars stay readable — their absence means
|
|
53
|
+
* "this generation could not say", which a consumer must treat as unknown
|
|
54
|
+
* rather than as complete.
|
|
55
|
+
*
|
|
56
|
+
* TRACE-F1 (0.36.x) — schemaVersion 6: the record gains `servedModel`, the
|
|
57
|
+
* id the SERVER says it served. Every generation before this one recorded
|
|
58
|
+
* only `model`, the id we asked for, and the two were assumed identical
|
|
59
|
+
* because nothing ever checked. A vendor is free to disagree: a retired
|
|
60
|
+
* name becomes an alias, a migration id resolves elsewhere, a tier is
|
|
61
|
+
* silently upgraded. The bench asked for `deepseek-v4-flash` on every leg
|
|
62
|
+
* for four days while the server answered as `deepseek-flash`, and the
|
|
63
|
+
* specified-vs-observed reconciliation built to catch precisely that
|
|
64
|
+
* reported nothing — it had no observed half to compare, so it compared
|
|
65
|
+
* the specification against itself.
|
|
66
|
+
*
|
|
67
|
+
* OPTIONAL, and absent means "the server stated nothing" — NEVER "the same
|
|
68
|
+
* as requested". That default is the whole defect: a field filled in with
|
|
69
|
+
* the request manufactures the agreement the reconciliation exists to
|
|
70
|
+
* test.
|
|
38
71
|
*/
|
|
39
|
-
/** schemaVersion:
|
|
72
|
+
/** schemaVersion: 6 for the `servedModel` statement (TRACE-F1);
|
|
73
|
+
* 5 was the `usageKnown` marker (F33-1). Version
|
|
40
74
|
* 1 = the 1.2.0 shape, 2 = the 1.3.0 shape, 3 = the 0.2.1 shape; all
|
|
41
75
|
* kept for generation-compat reads (R1d-1, R2-1). Algorithm and shape
|
|
42
76
|
* changes bump it (ADR-0051 §6 OUT-side versioning). */
|
|
43
|
-
export const TRACE_SCHEMA_VERSION =
|
|
77
|
+
export const TRACE_SCHEMA_VERSION = 6;
|
|
44
78
|
/** The versions a reader may meet in a ledger. v1 and v2 records are
|
|
45
79
|
* accepted (generation-compat) and read as defaults — no canonical
|
|
46
80
|
* block (v1), no rent block (v1, v2). */
|
|
47
|
-
|
|
81
|
+
/**
|
|
82
|
+
* EVERY generation ever written, named explicitly.
|
|
83
|
+
*
|
|
84
|
+
* F33-R1: this was `[1, 2, 3, TRACE_SCHEMA_VERSION]`, and a set spelled
|
|
85
|
+
* with a MOVING member drops the generation it was standing on every time
|
|
86
|
+
* the version rises. Raising 4 to 5 silently removed 4 — the version every
|
|
87
|
+
* trace the deployed 0.36.0 has ever written carries — so the reader
|
|
88
|
+
* stopped accepting the product's own live output while a v5 record went
|
|
89
|
+
* through. The whole generation-compat discipline exists to prevent that,
|
|
90
|
+
* and its own constant undid it.
|
|
91
|
+
*
|
|
92
|
+
* A new version is added here BY HAND, next to its field set and its hash
|
|
93
|
+
* spec. Three lists to extend is the point: a bump that forgets one is a
|
|
94
|
+
* bump that fails loudly rather than a reader that quietly narrows.
|
|
95
|
+
*/
|
|
96
|
+
export const TRACE_SCHEMA_VERSIONS = new Set([1, 2, 3, 4, 5, 6]);
|
|
48
97
|
import { PRICING_TABLE_V1, priceFor, pricingTableFor, validateCanonicalUsage } from "../usage/canonical.js";
|
|
49
98
|
import { validateRentLine } from "./rent.js";
|
|
50
99
|
export const HASH_SPEC_BY_VERSION = {
|
|
@@ -52,6 +101,8 @@ export const HASH_SPEC_BY_VERSION = {
|
|
|
52
101
|
2: { algorithm: "sha-256", output: "full-hex" }, // E2 — the algorithms do not change
|
|
53
102
|
3: { algorithm: "sha-256", output: "full-hex" }, // E3 — same algorithms, re-pinned (the E2 ritual)
|
|
54
103
|
4: { algorithm: "sha-256", output: "full-hex" }, // TUI2-R3v2 — `purpose` is a marker, not an input to any hash; re-pinned by the same ritual
|
|
104
|
+
5: { algorithm: "sha-256", output: "full-hex" }, // F33-1 — `usageKnown` is a marker, not an input to any hash; re-pinned by the same ritual
|
|
105
|
+
6: { algorithm: "sha-256", output: "full-hex" }, // TRACE-F1 — `servedModel` is the server's statement, not an input to any hash; re-pinned by the same ritual
|
|
55
106
|
};
|
|
56
107
|
export function hashSpecFor(version) {
|
|
57
108
|
const spec = HASH_SPEC_BY_VERSION[version];
|
|
@@ -100,9 +151,18 @@ export const TRACE_RECORD_FIELDS_V3 = [...TRACE_RECORD_FIELDS_V2, "rent"];
|
|
|
100
151
|
* TUI2-R3v2 ③: `purpose` is OPTIONAL — a run request omits it entirely,
|
|
101
152
|
* a side query names what it was for. It is listed in TRACE_RECORD_OPTIONAL
|
|
102
153
|
* below so the closed-set gate accepts a record without it. */
|
|
103
|
-
export const
|
|
154
|
+
export const TRACE_RECORD_FIELDS_V4 = [...TRACE_RECORD_FIELDS_V3, "purpose"];
|
|
155
|
+
/** The field set (schemaVersion 5) = the v4 set + `usageKnown` (F33-1).
|
|
156
|
+
* Optional for the same reason `purpose` is: requiring it would invalidate
|
|
157
|
+
* every v4 record ever written, and reading old generations is the whole
|
|
158
|
+
* point of the discipline. Absent means the generation could not say. */
|
|
159
|
+
export const TRACE_RECORD_FIELDS_V5 = [...TRACE_RECORD_FIELDS_V4, "usageKnown"];
|
|
160
|
+
/** TRACE-F1 (v6): the model the SERVER said it served. A v5 ledger has none
|
|
161
|
+
* on any record, which reads as "nobody asked the server" — the true
|
|
162
|
+
* statement about a ledger written before the adapters read the field. */
|
|
163
|
+
export const TRACE_RECORD_FIELDS = [...TRACE_RECORD_FIELDS_V5, "servedModel"];
|
|
104
164
|
/** The fields the closed-set check does not require to be present. */
|
|
105
|
-
export const TRACE_RECORD_OPTIONAL = ["purpose"];
|
|
165
|
+
export const TRACE_RECORD_OPTIONAL = ["purpose", "usageKnown", "servedModel"];
|
|
106
166
|
export const TRACE_SEGMENT_FIELDS = ["role", "seqRange", "estTokens", "freshness"];
|
|
107
167
|
// ── Validators ────────────────────────────────────────────────────────────
|
|
108
168
|
// Strict by design: extra keys are rejected (the closed set), so a
|
|
@@ -157,13 +217,37 @@ export function validateTraceRecord(v) {
|
|
|
157
217
|
// no `purpose` on any record, which reads as "every request was a run
|
|
158
218
|
// request", the true statement about a ledger written before side
|
|
159
219
|
// queries existed.
|
|
160
|
-
|
|
220
|
+
// F33-R1: the same moving-member defect lived here too. Every generation
|
|
221
|
+
// dispatches to ITS OWN field set; v4 is not "whatever the current one
|
|
222
|
+
// is minus a field", it is the set a v4 writer actually emitted.
|
|
223
|
+
if (typeof version !== "number" || !TRACE_SCHEMA_VERSIONS.has(version))
|
|
161
224
|
return false;
|
|
162
|
-
const fields = version === 1
|
|
225
|
+
const fields = version === 1
|
|
226
|
+
? TRACE_RECORD_FIELDS_V1
|
|
227
|
+
: version === 2
|
|
228
|
+
? TRACE_RECORD_FIELDS_V2
|
|
229
|
+
: version === 3
|
|
230
|
+
? TRACE_RECORD_FIELDS_V3
|
|
231
|
+
: version === 4
|
|
232
|
+
? TRACE_RECORD_FIELDS_V4
|
|
233
|
+
: version === 5
|
|
234
|
+
? TRACE_RECORD_FIELDS_V5
|
|
235
|
+
: TRACE_RECORD_FIELDS;
|
|
163
236
|
if (!hasClosedKeys(v, fields, ["lineageLink", ...TRACE_RECORD_OPTIONAL]))
|
|
164
237
|
return false;
|
|
165
238
|
if (v.purpose !== undefined && (typeof v.purpose !== "string" || v.purpose === ""))
|
|
166
239
|
return false;
|
|
240
|
+
// F33-R8: the completeness marker is a BOOLEAN. It was accepted as
|
|
241
|
+
// anything at all, so a record carrying `usageKnown: "yes"` validated and
|
|
242
|
+
// every consumer that branches on it read truthy — a marker a reader acts
|
|
243
|
+
// on has to be the type it claims to be.
|
|
244
|
+
if (v.usageKnown !== undefined && typeof v.usageKnown !== "boolean")
|
|
245
|
+
return false;
|
|
246
|
+
// TRACE-F1: the served id is a NON-EMPTY string when stated. Empty would
|
|
247
|
+
// reconcile as a contradiction against every requested id; absent is the
|
|
248
|
+
// honest "the server said nothing".
|
|
249
|
+
if (v.servedModel !== undefined && (typeof v.servedModel !== "string" || v.servedModel === ""))
|
|
250
|
+
return false;
|
|
167
251
|
if (v.kind !== "request")
|
|
168
252
|
return false;
|
|
169
253
|
if (typeof v.requestId !== "string" || typeof v.runId !== "string")
|
|
@@ -33,6 +33,10 @@ export interface RawUsage {
|
|
|
33
33
|
readonly outputTokens: number | null;
|
|
34
34
|
readonly cacheRead: number | null;
|
|
35
35
|
readonly cacheWrite: number | null;
|
|
36
|
+
/** RSN-1: absent when the provider reported no split. Optional rather
|
|
37
|
+
* than `number | null` so every existing caller stays valid and an
|
|
38
|
+
* omission cannot be mistaken for a measured zero. */
|
|
39
|
+
readonly reasoningTokens?: number;
|
|
36
40
|
}
|
|
37
41
|
/** The ONE canonical usage record. Every field has exactly one meaning
|
|
38
42
|
* (proposal §1.2). wall/ttft live in the trace record (latencyMs/ttftMs)
|
|
@@ -45,7 +49,17 @@ export interface CanonicalUsage {
|
|
|
45
49
|
readonly cacheRead: number;
|
|
46
50
|
/** null = the provider reports none (openai-compat honestly does). */
|
|
47
51
|
readonly cacheWrite: number | null;
|
|
48
|
-
/**
|
|
52
|
+
/** RSN-1: how much of `output` was THINKING, when the provider says.
|
|
53
|
+
* A SPLIT of the output, never a fifth quantity — completion tokens
|
|
54
|
+
* have always included reasoning ones, so the bill was never short;
|
|
55
|
+
* what was missing is which half the money went to, and that is the
|
|
56
|
+
* half the effort knob moves.
|
|
57
|
+
*
|
|
58
|
+
* null = not reported. The comment here used to say "reserved — every
|
|
59
|
+
* provider today", which was true when it was written and had stopped
|
|
60
|
+
* being true: the field sat hardcoded null while the vendor's own
|
|
61
|
+
* response carried `completion_tokens_details.reasoning_tokens`, read
|
|
62
|
+
* from the SAME object we already read the cache figure out of. */
|
|
49
63
|
readonly reasoning: number | null;
|
|
50
64
|
/** USD from the pricing table below — null is the R5b-④c ABSENT stamp:
|
|
51
65
|
* the table has no rate for this route (an injected table's hole is
|
package/dist/usage/canonical.js
CHANGED
|
@@ -101,7 +101,11 @@ export function canonicalizeUsage(route, raw, table = PRICING_TABLE_V1) {
|
|
|
101
101
|
output,
|
|
102
102
|
cacheRead,
|
|
103
103
|
cacheWrite,
|
|
104
|
-
|
|
104
|
+
// RSN-1: the reported split, or null when none was reported. NEVER 0
|
|
105
|
+
// for an absent one — a provider that measured no thinking and a
|
|
106
|
+
// provider that measured nothing are different facts, and this file
|
|
107
|
+
// spent a release conflating them under a comment that had aged out.
|
|
108
|
+
reasoning: raw.reasoningTokens ?? null,
|
|
105
109
|
costUsd: priceFor(route, { input, output, cacheRead, cacheWrite }, table),
|
|
106
110
|
pricingTableId: table.id,
|
|
107
111
|
pricingTableVersion: table.version,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "kiso runtime \u2014 durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"test": "vitest run"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@vincemakes/kiso-core": "0.
|
|
28
|
+
"@vincemakes/kiso-core": "0.37.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
33
|
-
"@vincemakes/kiso-provider-openai-responses": "0.
|
|
31
|
+
"@vincemakes/kiso-provider-anthropic": "0.37.0",
|
|
32
|
+
"@vincemakes/kiso-provider-openai": "0.37.0",
|
|
33
|
+
"@vincemakes/kiso-provider-openai-responses": "0.37.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@vincemakes/kiso-provider-anthropic": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@vincemakes/kiso-evals": "0.
|
|
47
|
+
"@vincemakes/kiso-evals": "0.37.0",
|
|
48
48
|
"@types/node": "^26.1.2",
|
|
49
49
|
"typescript": "^5.7.2",
|
|
50
50
|
"vitest": "^3.0.0"
|