@danypops/jittor 0.16.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -13
- package/docs/COMPACTION_EFFECTIVENESS.md +34 -0
- package/docs/CONTEXT_SNAPSHOTS.md +53 -0
- package/docs/MODEL_CATALOG.md +43 -0
- package/docs/OTLP_EXPORT.md +63 -0
- package/docs/PROVIDER_RESEARCH.md +1 -1
- package/docs/TOKEN_MEASUREMENT.md +53 -0
- package/docs/USAGE_BACKFILL.md +35 -0
- package/package.json +4 -3
- package/src/{providers/anthropic-contracts.ts → anthropic/rate-limits.ts} +1 -1
- package/src/{adapters/artificial-analysis-direct-source.ts → artificial-analysis/benchmark-source.ts} +3 -3
- package/src/cli-commands/backfill.ts +51 -0
- package/src/cli-commands/benchmarks.ts +4 -4
- package/src/cli-commands/catalog.ts +123 -0
- package/src/cli-commands/compaction.ts +1 -1
- package/src/cli-commands/context.ts +77 -4
- package/src/cli-commands/export.ts +30 -0
- package/src/cli-commands/metrics.ts +8 -2
- package/src/cli-commands/op.ts +1 -1
- package/src/cli-commands/route-args.ts +2 -2
- package/src/cli-commands/router.ts +2 -2
- package/src/cli-commands/support.ts +2 -2
- package/src/cli.ts +54 -21
- package/src/{providers/codex-contracts.ts → codex/contracts.ts} +1 -1
- package/src/codex/source.ts +53 -0
- package/src/{providers/codex.ts → codex/telemetry.ts} +2 -2
- package/src/constants.ts +18 -0
- package/src/daemon.ts +53 -23
- package/src/{providers/google-vertex-budget-contracts.ts → google-vertex/budget-contracts.ts} +3 -3
- package/src/{providers/google-vertex-budget.ts → google-vertex/budget-telemetry.ts} +6 -6
- package/src/{providers/google-vertex-contracts.ts → google-vertex/failures.ts} +1 -1
- package/src/google-vertex/source.ts +35 -0
- package/src/index.ts +135 -55
- package/src/{adapters/lmarena-hf-source.ts → lmarena/benchmark-source.ts} +3 -3
- package/src/observability/budget.ts +14 -0
- package/src/observability/context-delta.ts +309 -0
- package/src/{domain → observability}/context-hub.ts +19 -2
- package/src/observability/context-snapshot-history.ts +281 -0
- package/src/{domain → observability}/context-telemetry.ts +232 -4
- package/src/observability/model-identity.ts +42 -0
- package/src/{domain → observability}/model-observation.ts +26 -11
- package/src/{ports/metric-store.ts → observability/store.ts} +2 -2
- package/src/{ports → observability}/telemetry-source.ts +2 -2
- package/src/observability/token-measurement.ts +194 -0
- package/src/observability/usage-import.ts +122 -0
- package/src/openai/token-counter.ts +63 -0
- package/src/{adapters/openrouter-benchmark-source.ts → openrouter/benchmark-source.ts} +3 -3
- package/src/{providers/openrouter-contracts.ts → openrouter/contracts.ts} +1 -1
- package/src/{adapters/openrouter-design-arena-source.ts → openrouter/design-arena-source.ts} +4 -4
- package/src/openrouter/source.ts +20 -0
- package/src/{providers/openrouter.ts → openrouter/telemetry.ts} +2 -2
- package/src/{domain → optimization/model-selection}/benchmark.ts +9 -27
- package/src/optimization/model-selection/catalog-store.ts +97 -0
- package/src/optimization/model-selection/catalog.ts +478 -0
- package/src/{ports/benchmark-controller.ts → optimization/model-selection/controller.ts} +1 -1
- package/src/{adapters/metric-benchmark-store.ts → optimization/model-selection/observation-store.ts} +5 -5
- package/src/{domain/model-ranking-service.ts → optimization/model-selection/ranker.ts} +6 -12
- package/src/{domain/model-ranking.ts → optimization/model-selection/ranking.ts} +8 -2
- package/src/{ports/benchmark-source.ts → optimization/model-selection/source.ts} +1 -1
- package/src/{ports/benchmark-store.ts → optimization/model-selection/store.ts} +1 -1
- package/src/{domain/codex-recovery.ts → optimization/recovery/codex.ts} +1 -1
- package/src/{ports/router-controller.ts → optimization/routing/controller.ts} +1 -1
- package/src/{policy.ts → optimization/routing/policy.ts} +4 -14
- package/src/{router.ts → optimization/routing/router.ts} +4 -10
- package/src/otlp/exporter.ts +354 -0
- package/src/pi/session-usage-source.ts +203 -0
- package/src/{session-identity-service.ts → sessions/identity.ts} +2 -2
- package/src/{operations/session-scope.ts → sessions/router-authorization.ts} +1 -1
- package/src/{db.ts → sqlite/database.ts} +14 -1
- package/src/{adapters/sqlite-metric-store.ts → sqlite/metric-store.ts} +4 -4
- package/src/{adapters/sqlite-session-identity-store.ts → sqlite/session-store.ts} +1 -1
- package/src/sqlite/usage-import-store.ts +133 -0
- package/src/telemetry-export/exporter.ts +84 -0
- package/src/{operations → vehicle}/benchmark-operations.ts +3 -3
- package/src/vehicle/catalog-operations.ts +10 -0
- package/src/{client.ts → vehicle/client.ts} +1 -1
- package/src/{operations → vehicle}/context-operations.ts +13 -5
- package/src/vehicle/export-operations.ts +12 -0
- package/src/{operations/metrics-operations.ts → vehicle/metric-operations.ts} +4 -4
- package/src/{operations → vehicle}/model-ranking-operations.ts +3 -3
- package/src/{vehicle-registration.ts → vehicle/registration.ts} +14 -4
- package/src/{operations/router-operations.ts → vehicle/routing-operations.ts} +4 -4
- package/src/{service.ts → vehicle/service.ts} +108 -26
- package/src/{operations/session-identity-operations.ts → vehicle/session-operations.ts} +3 -3
- package/src/vehicle/usage-import-operations.ts +10 -0
- package/src/providers/telemetry-sources.ts +0 -108
- /package/src/{providers/google-adc-auth.ts → google-vertex/auth.ts} +0 -0
- /package/src/{domain → observability}/metric.ts +0 -0
- /package/src/{domain → observability}/task-cost.ts +0 -0
- /package/src/{domain → observability}/task-focus.ts +0 -0
- /package/src/{domain → observability}/usage.ts +0 -0
- /package/src/{config.ts → optimization/routing/config.ts} +0 -0
- /package/src/{ports/session-identity-store.ts → sessions/store.ts} +0 -0
- /package/src/{operations/types.ts → vehicle/operation-types.ts} +0 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
PAPYRUS_CONTEXT_INJECTION_SCHEMA,
|
|
8
8
|
} from "../constants.ts";
|
|
9
9
|
import type { MetricObservation, StoredMetricObservation } from "./metric.ts";
|
|
10
|
+
import type { TokenMeasurementProvenance } from "./token-measurement.ts";
|
|
10
11
|
|
|
11
12
|
interface PayloadSize {
|
|
12
13
|
characters: number;
|
|
@@ -133,16 +134,50 @@ export function papyrusContextMetric(observation: PapyrusContextInjection): Metr
|
|
|
133
134
|
};
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
export type CompactionMechanism = "pi-native" | "provider-side" | "extension";
|
|
138
|
+
|
|
136
139
|
export interface CompactionStart {
|
|
137
140
|
reason: "manual" | "threshold" | "overflow";
|
|
138
141
|
willRetry: boolean;
|
|
142
|
+
mechanism?: CompactionMechanism;
|
|
139
143
|
contextPercent?: number;
|
|
140
144
|
contextTokens?: number;
|
|
145
|
+
contextProvenance?: TokenMeasurementProvenance;
|
|
146
|
+
provider?: string;
|
|
147
|
+
model?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface CompactionCompletion extends Pick<CompactionStart, "reason" | "willRetry"> {
|
|
151
|
+
mechanism?: CompactionMechanism;
|
|
152
|
+
summaryTokens?: number;
|
|
153
|
+
summaryProvenance?: TokenMeasurementProvenance;
|
|
141
154
|
}
|
|
142
155
|
|
|
143
156
|
interface OpenCompaction extends CompactionStart {
|
|
157
|
+
mechanism: CompactionMechanism;
|
|
144
158
|
startedAt: number;
|
|
145
159
|
}
|
|
160
|
+
|
|
161
|
+
interface PendingEffectiveness {
|
|
162
|
+
preContextTokens: number;
|
|
163
|
+
preContextProvenance: TokenMeasurementProvenance;
|
|
164
|
+
mechanism: CompactionMechanism;
|
|
165
|
+
provider?: string;
|
|
166
|
+
model?: string;
|
|
167
|
+
summaryTokens?: number;
|
|
168
|
+
summaryProvenance?: TokenMeasurementProvenance;
|
|
169
|
+
completedAt: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface RegrowthState {
|
|
173
|
+
preContextTokens: number;
|
|
174
|
+
completedAt: number;
|
|
175
|
+
turns: number;
|
|
176
|
+
emitted: Set<number>;
|
|
177
|
+
mechanism: CompactionMechanism;
|
|
178
|
+
provider?: string;
|
|
179
|
+
model?: string;
|
|
180
|
+
}
|
|
146
181
|
interface UsageCounters {
|
|
147
182
|
turns: number;
|
|
148
183
|
injectedCharacters: number;
|
|
@@ -150,22 +185,42 @@ interface UsageCounters {
|
|
|
150
185
|
providerTokens: number;
|
|
151
186
|
cacheReadTokens: number;
|
|
152
187
|
cacheWriteTokens: number;
|
|
188
|
+
toolCalls: number;
|
|
189
|
+
toolFailures: number;
|
|
190
|
+
toolClasses: Map<string, number>;
|
|
191
|
+
acceptedOutcomes: number;
|
|
192
|
+
rejectedOutcomes: number;
|
|
153
193
|
}
|
|
154
194
|
|
|
155
195
|
function emptyCounters(): UsageCounters {
|
|
156
|
-
return {
|
|
196
|
+
return {
|
|
197
|
+
turns: 0,
|
|
198
|
+
injectedCharacters: 0,
|
|
199
|
+
estimatedInjectedTokens: 0,
|
|
200
|
+
providerTokens: 0,
|
|
201
|
+
cacheReadTokens: 0,
|
|
202
|
+
cacheWriteTokens: 0,
|
|
203
|
+
toolCalls: 0,
|
|
204
|
+
toolFailures: 0,
|
|
205
|
+
toolClasses: new Map(),
|
|
206
|
+
acceptedOutcomes: 0,
|
|
207
|
+
rejectedOutcomes: 0,
|
|
208
|
+
};
|
|
157
209
|
}
|
|
158
210
|
|
|
159
211
|
export class CompactionTelemetry {
|
|
160
212
|
private open: OpenCompaction | undefined;
|
|
161
213
|
private counters = emptyCounters();
|
|
162
214
|
private previousCompletedAt: number | undefined;
|
|
215
|
+
private pendingEffectiveness: PendingEffectiveness | undefined;
|
|
216
|
+
private regrowth: RegrowthState | undefined;
|
|
163
217
|
|
|
164
218
|
hasOpenCompaction(): boolean {
|
|
165
219
|
return this.open !== undefined;
|
|
166
220
|
}
|
|
167
221
|
observeTurn(): void {
|
|
168
222
|
this.counters.turns += 1;
|
|
223
|
+
if (this.regrowth) this.regrowth.turns += 1;
|
|
169
224
|
}
|
|
170
225
|
observeInjection(characters: number, estimatedTokens: number): void {
|
|
171
226
|
this.counters.injectedCharacters += Math.max(0, characters);
|
|
@@ -176,9 +231,31 @@ export class CompactionTelemetry {
|
|
|
176
231
|
this.counters.cacheReadTokens += Math.max(0, usage.cacheRead);
|
|
177
232
|
this.counters.cacheWriteTokens += Math.max(0, usage.cacheWrite);
|
|
178
233
|
}
|
|
234
|
+
observeToolClass(toolClass: string, failed: boolean): void {
|
|
235
|
+
if (!/^[a-z][a-z0-9-]{0,31}$/.test(toolClass)) return;
|
|
236
|
+
this.counters.toolCalls += 1;
|
|
237
|
+
if (failed) this.counters.toolFailures += 1;
|
|
238
|
+
if (this.counters.toolClasses.size < 16 || this.counters.toolClasses.has(toolClass))
|
|
239
|
+
this.counters.toolClasses.set(toolClass, (this.counters.toolClasses.get(toolClass) ?? 0) + 1);
|
|
240
|
+
}
|
|
241
|
+
observeOutcome(outcome: "accepted" | "rejected"): void {
|
|
242
|
+
if (outcome === "accepted") this.counters.acceptedOutcomes += 1;
|
|
243
|
+
else this.counters.rejectedOutcomes += 1;
|
|
244
|
+
}
|
|
179
245
|
|
|
180
246
|
begin(input: CompactionStart, now = Date.now()): MetricObservation {
|
|
181
|
-
this.open
|
|
247
|
+
if (this.open) {
|
|
248
|
+
return {
|
|
249
|
+
source: "pi-context",
|
|
250
|
+
scope: "compaction",
|
|
251
|
+
metric: "compaction-overlap",
|
|
252
|
+
value: 1,
|
|
253
|
+
unit: "count",
|
|
254
|
+
observedAt: now,
|
|
255
|
+
attributes: { reason: input.reason, willRetry: input.willRetry, openReason: this.open.reason },
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
this.open = { ...input, mechanism: input.mechanism ?? "pi-native", startedAt: now };
|
|
182
259
|
return {
|
|
183
260
|
source: "pi-context",
|
|
184
261
|
scope: "compaction",
|
|
@@ -190,7 +267,7 @@ export class CompactionTelemetry {
|
|
|
190
267
|
};
|
|
191
268
|
}
|
|
192
269
|
|
|
193
|
-
complete(input:
|
|
270
|
+
complete(input: CompactionCompletion, now = Date.now()): MetricObservation {
|
|
194
271
|
if (!this.open)
|
|
195
272
|
return {
|
|
196
273
|
source: "pi-context",
|
|
@@ -206,6 +283,18 @@ export class CompactionTelemetry {
|
|
|
206
283
|
const attributes = this.intervalAttributes(open, now);
|
|
207
284
|
this.previousCompletedAt = now;
|
|
208
285
|
this.counters = emptyCounters();
|
|
286
|
+
if (typeof open.contextTokens === "number" && Number.isSafeInteger(open.contextTokens) && open.contextTokens > 0) {
|
|
287
|
+
this.pendingEffectiveness = {
|
|
288
|
+
preContextTokens: open.contextTokens,
|
|
289
|
+
preContextProvenance: open.contextProvenance ?? "provider-reported",
|
|
290
|
+
mechanism: input.mechanism ?? open.mechanism,
|
|
291
|
+
...(open.provider === undefined ? {} : { provider: open.provider }),
|
|
292
|
+
...(open.model === undefined ? {} : { model: open.model }),
|
|
293
|
+
...(input.summaryTokens === undefined ? {} : { summaryTokens: Math.max(0, Math.round(input.summaryTokens)) }),
|
|
294
|
+
...(input.summaryProvenance === undefined ? {} : { summaryProvenance: input.summaryProvenance }),
|
|
295
|
+
completedAt: now,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
209
298
|
return {
|
|
210
299
|
source: "pi-context",
|
|
211
300
|
scope: "compaction",
|
|
@@ -213,10 +302,108 @@ export class CompactionTelemetry {
|
|
|
213
302
|
value: Math.max(0, now - open.startedAt),
|
|
214
303
|
unit: "milliseconds",
|
|
215
304
|
observedAt: now,
|
|
216
|
-
attributes: {
|
|
305
|
+
attributes: {
|
|
306
|
+
...attributes,
|
|
307
|
+
reason: input.reason,
|
|
308
|
+
willRetry: input.willRetry,
|
|
309
|
+
mechanism: input.mechanism ?? open.mechanism,
|
|
310
|
+
...(input.summaryTokens === undefined ? {} : { summaryTokens: Math.max(0, Math.round(input.summaryTokens)) }),
|
|
311
|
+
...(input.summaryProvenance === undefined ? {} : { summaryProvenance: input.summaryProvenance }),
|
|
312
|
+
},
|
|
217
313
|
};
|
|
218
314
|
}
|
|
219
315
|
|
|
316
|
+
/** Correlates the first post-compaction request, then reports bounded 50/80/100% regrowth milestones once each. */
|
|
317
|
+
observeContextSnapshot(
|
|
318
|
+
tokens: number,
|
|
319
|
+
provenance: TokenMeasurementProvenance,
|
|
320
|
+
now = Date.now(),
|
|
321
|
+
identity?: { provider: string; model: string },
|
|
322
|
+
): MetricObservation[] {
|
|
323
|
+
if (!Number.isSafeInteger(tokens) || tokens < 0) return [];
|
|
324
|
+
const observations: MetricObservation[] = [];
|
|
325
|
+
if (this.pendingEffectiveness) {
|
|
326
|
+
const pending = this.pendingEffectiveness;
|
|
327
|
+
this.pendingEffectiveness = undefined;
|
|
328
|
+
const identityChange =
|
|
329
|
+
identity && pending.provider && identity.provider !== pending.provider
|
|
330
|
+
? "provider-changed"
|
|
331
|
+
: identity && pending.model && identity.model !== pending.model
|
|
332
|
+
? "model-changed"
|
|
333
|
+
: null;
|
|
334
|
+
if (identityChange) {
|
|
335
|
+
this.regrowth = undefined;
|
|
336
|
+
return [
|
|
337
|
+
{
|
|
338
|
+
source: "pi-context",
|
|
339
|
+
scope: "compaction",
|
|
340
|
+
metric: "compaction-effectiveness-unavailable",
|
|
341
|
+
value: 1,
|
|
342
|
+
unit: "count",
|
|
343
|
+
observedAt: now,
|
|
344
|
+
attributes: { reason: identityChange, mechanism: pending.mechanism },
|
|
345
|
+
},
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
const reduction = (pending.preContextTokens - tokens) / pending.preContextTokens;
|
|
349
|
+
observations.push({
|
|
350
|
+
source: "pi-context",
|
|
351
|
+
scope: "compaction",
|
|
352
|
+
metric: "compaction-effectiveness",
|
|
353
|
+
value: reduction,
|
|
354
|
+
unit: "ratio",
|
|
355
|
+
observedAt: now,
|
|
356
|
+
attributes: {
|
|
357
|
+
mechanism: pending.mechanism,
|
|
358
|
+
preContextTokens: pending.preContextTokens,
|
|
359
|
+
postContextTokens: tokens,
|
|
360
|
+
preContextProvenance: pending.preContextProvenance,
|
|
361
|
+
postContextProvenance: provenance,
|
|
362
|
+
...(pending.provider === undefined ? {} : { provider: pending.provider }),
|
|
363
|
+
...(pending.model === undefined ? {} : { model: pending.model }),
|
|
364
|
+
...(pending.summaryTokens === undefined ? {} : { summaryTokens: pending.summaryTokens }),
|
|
365
|
+
...(pending.summaryProvenance === undefined ? {} : { summaryProvenance: pending.summaryProvenance }),
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
this.regrowth = {
|
|
369
|
+
preContextTokens: pending.preContextTokens,
|
|
370
|
+
completedAt: pending.completedAt,
|
|
371
|
+
turns: 0,
|
|
372
|
+
emitted: new Set(),
|
|
373
|
+
mechanism: pending.mechanism,
|
|
374
|
+
...(pending.provider === undefined ? {} : { provider: pending.provider }),
|
|
375
|
+
...(pending.model === undefined ? {} : { model: pending.model }),
|
|
376
|
+
};
|
|
377
|
+
return observations;
|
|
378
|
+
}
|
|
379
|
+
const regrowth = this.regrowth;
|
|
380
|
+
if (!regrowth) return observations;
|
|
381
|
+
const fraction = tokens / regrowth.preContextTokens;
|
|
382
|
+
for (const threshold of [0.5, 0.8, 1]) {
|
|
383
|
+
if (fraction < threshold || regrowth.emitted.has(threshold)) continue;
|
|
384
|
+
regrowth.emitted.add(threshold);
|
|
385
|
+
observations.push({
|
|
386
|
+
source: "pi-context",
|
|
387
|
+
scope: "compaction",
|
|
388
|
+
metric: "compaction-regrowth",
|
|
389
|
+
value: threshold,
|
|
390
|
+
unit: "ratio",
|
|
391
|
+
observedAt: now,
|
|
392
|
+
attributes: {
|
|
393
|
+
threshold,
|
|
394
|
+
contextTokens: tokens,
|
|
395
|
+
contextProvenance: provenance,
|
|
396
|
+
turnsSinceCompaction: regrowth.turns,
|
|
397
|
+
elapsedSinceCompactionMs: Math.max(0, now - regrowth.completedAt),
|
|
398
|
+
mechanism: regrowth.mechanism,
|
|
399
|
+
...(regrowth.provider === undefined ? {} : { provider: regrowth.provider }),
|
|
400
|
+
...(regrowth.model === undefined ? {} : { model: regrowth.model }),
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return observations;
|
|
405
|
+
}
|
|
406
|
+
|
|
220
407
|
abort(now = Date.now(), abortReason = "aborted"): MetricObservation {
|
|
221
408
|
const open = this.open;
|
|
222
409
|
this.open = undefined;
|
|
@@ -247,6 +434,10 @@ export class CompactionTelemetry {
|
|
|
247
434
|
return {
|
|
248
435
|
reason: open.reason,
|
|
249
436
|
willRetry: open.willRetry,
|
|
437
|
+
mechanism: open.mechanism,
|
|
438
|
+
...(open.contextProvenance === undefined ? {} : { contextProvenance: open.contextProvenance }),
|
|
439
|
+
...(open.provider === undefined ? {} : { provider: open.provider }),
|
|
440
|
+
...(open.model === undefined ? {} : { model: open.model }),
|
|
250
441
|
...(open.contextPercent === undefined ? {} : { contextPercent: open.contextPercent }),
|
|
251
442
|
...(open.contextTokens === undefined ? {} : { contextTokens: open.contextTokens }),
|
|
252
443
|
turnsSincePrevious: this.counters.turns,
|
|
@@ -255,6 +446,13 @@ export class CompactionTelemetry {
|
|
|
255
446
|
providerTokensSincePrevious: this.counters.providerTokens,
|
|
256
447
|
cacheReadTokensSincePrevious: this.counters.cacheReadTokens,
|
|
257
448
|
cacheWriteTokensSincePrevious: this.counters.cacheWriteTokens,
|
|
449
|
+
toolCallsSincePrevious: this.counters.toolCalls,
|
|
450
|
+
toolFailuresSincePrevious: this.counters.toolFailures,
|
|
451
|
+
repeatedToolClassesSincePrevious: [...this.counters.toolClasses.entries()]
|
|
452
|
+
.filter(([, count]) => count > 1)
|
|
453
|
+
.map(([name, count]) => `${name}:${count}`),
|
|
454
|
+
acceptedOutcomesSincePrevious: this.counters.acceptedOutcomes,
|
|
455
|
+
rejectedOutcomesSincePrevious: this.counters.rejectedOutcomes,
|
|
258
456
|
...(this.previousCompletedAt === undefined ? {} : { elapsedSincePreviousMs: Math.max(0, now - this.previousCompletedAt) }),
|
|
259
457
|
};
|
|
260
458
|
}
|
|
@@ -285,6 +483,12 @@ export interface ContextAssessment {
|
|
|
285
483
|
averageElapsedMsBetween: number | null;
|
|
286
484
|
averageProviderTokensBetween: number | null;
|
|
287
485
|
averageCacheReadTokensBetween: number | null;
|
|
486
|
+
effectivenessSamples: number;
|
|
487
|
+
averageReductionRatio: number | null;
|
|
488
|
+
averagePreContextTokens: number | null;
|
|
489
|
+
averagePostContextTokens: number | null;
|
|
490
|
+
mechanisms: Record<CompactionMechanism, number>;
|
|
491
|
+
regrowth: Record<"50" | "80" | "100", { samples: number; averageTurns: number | null; averageElapsedMs: number | null } | null>;
|
|
288
492
|
reasons: Record<"manual" | "threshold" | "overflow", number>;
|
|
289
493
|
};
|
|
290
494
|
}
|
|
@@ -315,6 +519,24 @@ export function assessContextTelemetry(
|
|
|
315
519
|
const unchanged = injections.filter((row) => row.attributes.unchanged === true).length;
|
|
316
520
|
const completed = compactions.filter((row) => row.metric === "compaction-duration" && typeof row.value === "number");
|
|
317
521
|
const aborted = compactions.filter((row) => row.metric === "compaction-aborted");
|
|
522
|
+
const effectiveness = compactions.filter(
|
|
523
|
+
(row) => row.metric === "compaction-effectiveness" && typeof row.value === "number" && Number.isFinite(row.value),
|
|
524
|
+
);
|
|
525
|
+
const regrowthRows = compactions.filter((row) => row.metric === "compaction-regrowth");
|
|
526
|
+
const mechanisms: Record<CompactionMechanism, number> = { "pi-native": 0, "provider-side": 0, extension: 0 };
|
|
527
|
+
for (const row of effectiveness) {
|
|
528
|
+
const mechanism = row.attributes.mechanism;
|
|
529
|
+
if (mechanism === "pi-native" || mechanism === "provider-side" || mechanism === "extension") mechanisms[mechanism] += 1;
|
|
530
|
+
}
|
|
531
|
+
const regrowth = (threshold: number): { samples: number; averageTurns: number | null; averageElapsedMs: number | null } | null => {
|
|
532
|
+
const rows = regrowthRows.filter((row) => row.attributes.threshold === threshold);
|
|
533
|
+
if (rows.length === 0) return null;
|
|
534
|
+
return {
|
|
535
|
+
samples: rows.length,
|
|
536
|
+
averageTurns: average(rows.flatMap((row) => numericAttribute(row, "turnsSinceCompaction") ?? [])),
|
|
537
|
+
averageElapsedMs: average(rows.flatMap((row) => numericAttribute(row, "elapsedSinceCompactionMs") ?? [])),
|
|
538
|
+
};
|
|
539
|
+
};
|
|
318
540
|
const reasons = { manual: 0, threshold: 0, overflow: 0 };
|
|
319
541
|
for (const row of completed) {
|
|
320
542
|
const reason = row.attributes.reason;
|
|
@@ -347,6 +569,12 @@ export function assessContextTelemetry(
|
|
|
347
569
|
averageElapsedMsBetween: average(completed.flatMap((row) => numericAttribute(row, "elapsedSincePreviousMs") ?? [])),
|
|
348
570
|
averageProviderTokensBetween: average(completed.flatMap((row) => numericAttribute(row, "providerTokensSincePrevious") ?? [])),
|
|
349
571
|
averageCacheReadTokensBetween: average(completed.flatMap((row) => numericAttribute(row, "cacheReadTokensSincePrevious") ?? [])),
|
|
572
|
+
effectivenessSamples: effectiveness.length,
|
|
573
|
+
averageReductionRatio: average(effectiveness.map((row) => row.value as number)),
|
|
574
|
+
averagePreContextTokens: average(effectiveness.flatMap((row) => numericAttribute(row, "preContextTokens") ?? [])),
|
|
575
|
+
averagePostContextTokens: average(effectiveness.flatMap((row) => numericAttribute(row, "postContextTokens") ?? [])),
|
|
576
|
+
mechanisms,
|
|
577
|
+
regrowth: { "50": regrowth(0.5), "80": regrowth(0.8), "100": regrowth(1) },
|
|
350
578
|
reasons,
|
|
351
579
|
},
|
|
352
580
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BENCHMARK_IDENTITY_MAX_CHARACTERS } from "../constants.ts";
|
|
2
|
+
|
|
3
|
+
const VERSION_SUFFIX = /-(\d{4}-\d{2}-\d{2})$/;
|
|
4
|
+
|
|
5
|
+
export interface ModelIdentity {
|
|
6
|
+
provider: string;
|
|
7
|
+
model: string;
|
|
8
|
+
version: string | null;
|
|
9
|
+
canonical: string;
|
|
10
|
+
aliases: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function identityText(value: string, name: string): string {
|
|
14
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${name} is required`);
|
|
15
|
+
const normalized = value.trim().toLowerCase();
|
|
16
|
+
if (normalized.length > BENCHMARK_IDENTITY_MAX_CHARACTERS) throw new Error(`${name} exceeds the length limit`);
|
|
17
|
+
if (/\p{Cc}/u.test(normalized)) throw new Error(`${name} contains control characters`);
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function identityPart(value: string, name: string, allowPath = false): string {
|
|
22
|
+
const normalized = identityText(value, name);
|
|
23
|
+
const pattern = allowPath ? /^[a-z0-9][a-z0-9._:+/-]*$/ : /^[a-z0-9][a-z0-9._:+-]*$/;
|
|
24
|
+
if (!pattern.test(normalized) || normalized.includes("//")) throw new Error(`${name} is invalid`);
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeModelIdentity(provider: string, model: string, aliases: string[] = []): ModelIdentity {
|
|
29
|
+
const normalizedProvider = identityPart(provider, "provider");
|
|
30
|
+
const normalizedModel = identityPart(model, "model", true);
|
|
31
|
+
const canonical = `${normalizedProvider}/${normalizedModel}`;
|
|
32
|
+
const normalizedAliases = [...new Set(aliases.map((alias) => identityText(alias, "alias")))]
|
|
33
|
+
.filter((alias) => alias !== canonical)
|
|
34
|
+
.sort();
|
|
35
|
+
return {
|
|
36
|
+
provider: normalizedProvider,
|
|
37
|
+
model: normalizedModel,
|
|
38
|
+
version: VERSION_SUFFIX.exec(normalizedModel)?.[1] ?? null,
|
|
39
|
+
canonical,
|
|
40
|
+
aliases: normalizedAliases,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
MODEL_OBSERVATION_FRESH_MS,
|
|
5
5
|
MODEL_OBSERVATION_IDENTITY_MAX_CHARACTERS,
|
|
6
6
|
} from "../constants.ts";
|
|
7
|
-
import { normalizeModelIdentity } from "./benchmark.ts";
|
|
8
7
|
import type { MetricObservation, MetricUnit, StoredMetricObservation } from "./metric.ts";
|
|
8
|
+
import { normalizeModelIdentity } from "./model-identity.ts";
|
|
9
|
+
import type { TokenMeasurementScope } from "./token-measurement.ts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Two independent axes, not one flat class: "coding" is a subject-matter domain (which
|
|
@@ -131,10 +132,10 @@ export function validateModelRunObservation(value: unknown): ModelRunObservation
|
|
|
131
132
|
startedAt,
|
|
132
133
|
firstTokenAt,
|
|
133
134
|
completedAt,
|
|
134
|
-
inputTokens: nonNegative(input.inputTokens, "input tokens"),
|
|
135
|
-
outputTokens: nonNegative(input.outputTokens, "output tokens"),
|
|
136
|
-
cacheReadTokens: nonNegative(input.cacheReadTokens, "cache read tokens"),
|
|
137
|
-
cacheWriteTokens: nonNegative(input.cacheWriteTokens, "cache write tokens"),
|
|
135
|
+
inputTokens: nonNegative(input.inputTokens, "input tokens", true),
|
|
136
|
+
outputTokens: nonNegative(input.outputTokens, "output tokens", true),
|
|
137
|
+
cacheReadTokens: nonNegative(input.cacheReadTokens, "cache read tokens", true),
|
|
138
|
+
cacheWriteTokens: nonNegative(input.cacheWriteTokens, "cache write tokens", true),
|
|
138
139
|
costUsd: nonNegative(input.costUsd, "cost"),
|
|
139
140
|
providerResponses,
|
|
140
141
|
toolCalls,
|
|
@@ -174,14 +175,28 @@ export function modelRunMetrics(value: ModelRunObservation): MetricObservation[]
|
|
|
174
175
|
type: run.type,
|
|
175
176
|
runId: run.runId,
|
|
176
177
|
};
|
|
177
|
-
const metric = (name: string, amount: number, unit: MetricUnit): MetricObservation => ({
|
|
178
|
+
const metric = (name: string, amount: number, unit: MetricUnit, tokenScope?: TokenMeasurementScope): MetricObservation => ({
|
|
178
179
|
source: "local-model",
|
|
179
180
|
scope,
|
|
180
181
|
metric: name,
|
|
181
182
|
value: amount,
|
|
182
183
|
unit,
|
|
183
184
|
observedAt: run.completedAt,
|
|
184
|
-
attributes
|
|
185
|
+
attributes: {
|
|
186
|
+
...attributes,
|
|
187
|
+
...(tokenScope
|
|
188
|
+
? {
|
|
189
|
+
tokenMeasurement: {
|
|
190
|
+
tokens: amount,
|
|
191
|
+
scope: tokenScope,
|
|
192
|
+
provenance: "provider-reported",
|
|
193
|
+
method: "pi-assistant-usage",
|
|
194
|
+
provider: run.provider,
|
|
195
|
+
model: run.model,
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
: {}),
|
|
199
|
+
},
|
|
185
200
|
});
|
|
186
201
|
const wallMs = run.completedAt - run.startedAt;
|
|
187
202
|
const totalInput = run.inputTokens + run.cacheReadTokens;
|
|
@@ -190,10 +205,10 @@ export function modelRunMetrics(value: ModelRunObservation): MetricObservation[]
|
|
|
190
205
|
metrics.push(
|
|
191
206
|
metric("wall-latency", wallMs, "milliseconds"),
|
|
192
207
|
metric("output-throughput", wallMs === 0 ? 0 : run.outputTokens / (wallMs / 1_000), "tokens-per-second"),
|
|
193
|
-
metric("input-tokens", run.inputTokens, "tokens"),
|
|
194
|
-
metric("output-tokens", run.outputTokens, "tokens"),
|
|
195
|
-
metric("cache-read-tokens", run.cacheReadTokens, "tokens"),
|
|
196
|
-
metric("cache-write-tokens", run.cacheWriteTokens, "tokens"),
|
|
208
|
+
metric("input-tokens", run.inputTokens, "tokens", "request-input"),
|
|
209
|
+
metric("output-tokens", run.outputTokens, "tokens", "response-output"),
|
|
210
|
+
metric("cache-read-tokens", run.cacheReadTokens, "tokens", "cache-read"),
|
|
211
|
+
metric("cache-write-tokens", run.cacheWriteTokens, "tokens", "cache-write"),
|
|
197
212
|
metric("cache-read-ratio", totalInput === 0 ? 0 : run.cacheReadTokens / totalInput, "ratio"),
|
|
198
213
|
metric("cost", run.costUsd, "usd"),
|
|
199
214
|
metric("provider-responses", run.providerResponses, "count"),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { MetricObservation, MetricQuery, StoredMetricObservation } from "
|
|
2
|
-
import type { UsageAggregateRow } from "
|
|
1
|
+
import type { MetricObservation, MetricQuery, StoredMetricObservation } from "./metric.ts";
|
|
2
|
+
import type { UsageAggregateRow } from "./usage.ts";
|
|
3
3
|
|
|
4
4
|
export interface DistinctScopesFilter {
|
|
5
5
|
source: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { BudgetWindow } from "./budget.ts";
|
|
2
|
+
import type { MetricObservation } from "./metric.ts";
|
|
3
3
|
|
|
4
4
|
export interface TelemetryBatch {
|
|
5
5
|
observedAt: number;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN } from "../constants.ts";
|
|
2
|
+
import { normalizeModelIdentity } from "./model-identity.ts";
|
|
3
|
+
|
|
4
|
+
export const TOKEN_MEASUREMENT_PROVENANCES = [
|
|
5
|
+
"provider-reported",
|
|
6
|
+
"provider-count-api",
|
|
7
|
+
"tokenizer-exact-text",
|
|
8
|
+
"structural-estimate",
|
|
9
|
+
] as const;
|
|
10
|
+
export type TokenMeasurementProvenance = (typeof TOKEN_MEASUREMENT_PROVENANCES)[number];
|
|
11
|
+
|
|
12
|
+
export const TOKEN_MEASUREMENT_SCOPES = [
|
|
13
|
+
"request-context",
|
|
14
|
+
"request-input",
|
|
15
|
+
"response-output",
|
|
16
|
+
"cache-read",
|
|
17
|
+
"cache-write",
|
|
18
|
+
"context-item",
|
|
19
|
+
"unattributed-residual",
|
|
20
|
+
] as const;
|
|
21
|
+
export type TokenMeasurementScope = (typeof TOKEN_MEASUREMENT_SCOPES)[number];
|
|
22
|
+
|
|
23
|
+
export interface TokenMeasurement {
|
|
24
|
+
tokens: number;
|
|
25
|
+
scope: TokenMeasurementScope;
|
|
26
|
+
provenance: TokenMeasurementProvenance;
|
|
27
|
+
/** Bounded implementation/counting identity, never content (for example `char/4` or `gpt-tokenizer:o200k_base`). */
|
|
28
|
+
method: string;
|
|
29
|
+
provider?: string;
|
|
30
|
+
model?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TOKEN_MEASUREMENT_FIELDS = new Set<keyof TokenMeasurement>(["tokens", "scope", "provenance", "method", "provider", "model"]);
|
|
34
|
+
const TOKEN_MEASUREMENT_METHOD_MAX_CHARACTERS = 120;
|
|
35
|
+
|
|
36
|
+
function boundedMethod(value: unknown): string {
|
|
37
|
+
if (typeof value !== "string" || value.length === 0 || value.length > TOKEN_MEASUREMENT_METHOD_MAX_CHARACTERS || /\p{Cc}/u.test(value))
|
|
38
|
+
throw new Error("token measurement method is invalid");
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateTokenMeasurement(value: unknown): TokenMeasurement {
|
|
43
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("token measurement must be an object");
|
|
44
|
+
const input = value as Record<string, unknown>;
|
|
45
|
+
for (const key of Object.keys(input)) {
|
|
46
|
+
if (!TOKEN_MEASUREMENT_FIELDS.has(key as keyof TokenMeasurement)) throw new Error(`unsupported field: ${key}`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof input.tokens !== "number" || !Number.isSafeInteger(input.tokens) || input.tokens < 0)
|
|
49
|
+
throw new Error("token measurement tokens are invalid");
|
|
50
|
+
if (!TOKEN_MEASUREMENT_SCOPES.includes(input.scope as TokenMeasurementScope)) throw new Error("token measurement scope is invalid");
|
|
51
|
+
if (!TOKEN_MEASUREMENT_PROVENANCES.includes(input.provenance as TokenMeasurementProvenance))
|
|
52
|
+
throw new Error("token measurement provenance is invalid");
|
|
53
|
+
if ((input.provider === undefined) !== (input.model === undefined))
|
|
54
|
+
throw new Error("token measurement provider/model identity is incomplete");
|
|
55
|
+
if (
|
|
56
|
+
(input.provider !== undefined && typeof input.provider !== "string") ||
|
|
57
|
+
(input.model !== undefined && typeof input.model !== "string")
|
|
58
|
+
)
|
|
59
|
+
throw new Error("token measurement provider/model identity is invalid");
|
|
60
|
+
const identity =
|
|
61
|
+
input.provider === undefined || input.model === undefined ? undefined : normalizeModelIdentity(input.provider, input.model);
|
|
62
|
+
return {
|
|
63
|
+
tokens: input.tokens,
|
|
64
|
+
scope: input.scope as TokenMeasurementScope,
|
|
65
|
+
provenance: input.provenance as TokenMeasurementProvenance,
|
|
66
|
+
method: boundedMethod(input.method),
|
|
67
|
+
...(identity ? { provider: identity.provider, model: identity.model } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface TextTokenCountInput {
|
|
72
|
+
/** Ephemeral input. Implementations must not retain or include it in diagnostics/measurements. */
|
|
73
|
+
text: string;
|
|
74
|
+
provider?: string;
|
|
75
|
+
model?: string;
|
|
76
|
+
scope: "context-item" | "request-input" | "response-output";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A replaceable, content-ephemeral text counting boundary. Unsupported provider/model identities return null. */
|
|
80
|
+
export interface TextTokenCounter {
|
|
81
|
+
countText(input: TextTokenCountInput): TokenMeasurement | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class StructuralTextTokenCounter implements TextTokenCounter {
|
|
85
|
+
countText(input: TextTokenCountInput): TokenMeasurement {
|
|
86
|
+
const identity =
|
|
87
|
+
input.provider === undefined || input.model === undefined ? undefined : normalizeModelIdentity(input.provider, input.model);
|
|
88
|
+
return {
|
|
89
|
+
tokens: Math.ceil(input.text.length / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN),
|
|
90
|
+
scope: input.scope,
|
|
91
|
+
provenance: "structural-estimate",
|
|
92
|
+
method: `char/${CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN}`,
|
|
93
|
+
...(identity ? { provider: identity.provider, model: identity.model } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Uses the first supporting model-aware counter and fails open to the explicit structural fallback. */
|
|
99
|
+
export function countTextWithFallback(
|
|
100
|
+
input: TextTokenCountInput,
|
|
101
|
+
counters: readonly TextTokenCounter[],
|
|
102
|
+
fallback: TextTokenCounter = new StructuralTextTokenCounter(),
|
|
103
|
+
): TokenMeasurement {
|
|
104
|
+
for (const counter of counters) {
|
|
105
|
+
try {
|
|
106
|
+
const measurement = counter.countText(input);
|
|
107
|
+
if (measurement) return validateTokenMeasurement(measurement);
|
|
108
|
+
} catch {
|
|
109
|
+
// Counting is observational. Unknown mappings/tokenizer failures must remain visibly estimated, never block the request.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const measurement = fallback.countText(input);
|
|
113
|
+
if (!measurement) throw new Error("structural token counter did not return a measurement");
|
|
114
|
+
return validateTokenMeasurement(measurement);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface RequestTokenReconciliation {
|
|
118
|
+
aggregate: TokenMeasurement;
|
|
119
|
+
attributedTokens: number;
|
|
120
|
+
residual: TokenMeasurement;
|
|
121
|
+
overshootTokens: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const REQUEST_RECONCILIATION_FIELDS = new Set<keyof RequestTokenReconciliation>([
|
|
125
|
+
"aggregate",
|
|
126
|
+
"attributedTokens",
|
|
127
|
+
"residual",
|
|
128
|
+
"overshootTokens",
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
export function validateRequestTokenReconciliation(value: unknown): RequestTokenReconciliation {
|
|
132
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
133
|
+
throw new Error("request token reconciliation must be an object");
|
|
134
|
+
const input = value as Record<string, unknown>;
|
|
135
|
+
for (const key of Object.keys(input)) {
|
|
136
|
+
if (!REQUEST_RECONCILIATION_FIELDS.has(key as keyof RequestTokenReconciliation))
|
|
137
|
+
throw new Error(`request token reconciliation contains unsupported field: ${key}`);
|
|
138
|
+
}
|
|
139
|
+
const aggregate = validateTokenMeasurement(input.aggregate);
|
|
140
|
+
const residual = validateTokenMeasurement(input.residual);
|
|
141
|
+
if (aggregate.scope !== "request-context" && aggregate.scope !== "request-input")
|
|
142
|
+
throw new Error("request token aggregate scope is invalid");
|
|
143
|
+
if (
|
|
144
|
+
residual.scope !== "unattributed-residual" ||
|
|
145
|
+
residual.provenance !== "structural-estimate" ||
|
|
146
|
+
residual.method !== "aggregate-minus-attributed"
|
|
147
|
+
)
|
|
148
|
+
throw new Error("request token residual provenance is invalid");
|
|
149
|
+
if (residual.provider !== aggregate.provider || residual.model !== aggregate.model)
|
|
150
|
+
throw new Error("request token residual identity does not match aggregate identity");
|
|
151
|
+
const attributedTokens = input.attributedTokens;
|
|
152
|
+
const overshootTokens = input.overshootTokens;
|
|
153
|
+
if (typeof attributedTokens !== "number" || !Number.isSafeInteger(attributedTokens) || attributedTokens < 0)
|
|
154
|
+
throw new Error("request token attributed count is invalid");
|
|
155
|
+
if (typeof overshootTokens !== "number" || !Number.isSafeInteger(overshootTokens) || overshootTokens < 0)
|
|
156
|
+
throw new Error("request token overshoot count is invalid");
|
|
157
|
+
if (residual.tokens !== Math.max(0, aggregate.tokens - attributedTokens)) throw new Error("request token residual is inconsistent");
|
|
158
|
+
if (overshootTokens !== Math.max(0, attributedTokens - aggregate.tokens)) throw new Error("request token overshoot is inconsistent");
|
|
159
|
+
return { aggregate, attributedTokens, residual, overshootTokens };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Reconciles local item measurements against an authoritative request aggregate without spreading
|
|
164
|
+
* its remainder across those items. The residual stays explicit and estimate-provenance because
|
|
165
|
+
* provider envelope/media/rewrites and mixed local counting methods prevent exact attribution.
|
|
166
|
+
*/
|
|
167
|
+
export function reconcileRequestTokens(
|
|
168
|
+
aggregateValue: TokenMeasurement,
|
|
169
|
+
attributedValues: readonly TokenMeasurement[],
|
|
170
|
+
): RequestTokenReconciliation {
|
|
171
|
+
const aggregate = validateTokenMeasurement(aggregateValue);
|
|
172
|
+
if (aggregate.scope !== "request-context" && aggregate.scope !== "request-input")
|
|
173
|
+
throw new Error("request token aggregate scope is invalid");
|
|
174
|
+
const attributed = attributedValues.map(validateTokenMeasurement);
|
|
175
|
+
if (attributed.some((measurement) => measurement.scope !== "context-item")) throw new Error("request token attribution scope is invalid");
|
|
176
|
+
const attributedTokens = attributed.reduce((sum, measurement) => {
|
|
177
|
+
const next = sum + measurement.tokens;
|
|
178
|
+
if (!Number.isSafeInteger(next)) throw new Error("request token attribution sum is invalid");
|
|
179
|
+
return next;
|
|
180
|
+
}, 0);
|
|
181
|
+
const difference = aggregate.tokens - attributedTokens;
|
|
182
|
+
return {
|
|
183
|
+
aggregate,
|
|
184
|
+
attributedTokens,
|
|
185
|
+
residual: {
|
|
186
|
+
tokens: Math.max(0, difference),
|
|
187
|
+
scope: "unattributed-residual",
|
|
188
|
+
provenance: "structural-estimate",
|
|
189
|
+
method: "aggregate-minus-attributed",
|
|
190
|
+
...(aggregate.provider && aggregate.model ? { provider: aggregate.provider, model: aggregate.model } : {}),
|
|
191
|
+
},
|
|
192
|
+
overshootTokens: Math.max(0, -difference),
|
|
193
|
+
};
|
|
194
|
+
}
|