@danypops/jittor 0.14.0 → 0.16.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.
Files changed (59) hide show
  1. package/package.json +4 -3
  2. package/src/adapters/artificial-analysis-direct-source.ts +42 -11
  3. package/src/adapters/lmarena-hf-source.ts +37 -15
  4. package/src/adapters/metric-benchmark-store.ts +29 -18
  5. package/src/adapters/openrouter-benchmark-source.ts +75 -19
  6. package/src/adapters/openrouter-design-arena-source.ts +36 -19
  7. package/src/adapters/sqlite-metric-store.ts +48 -25
  8. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  9. package/src/cli-commands/benchmarks.ts +87 -22
  10. package/src/cli-commands/compaction.ts +7 -2
  11. package/src/cli-commands/context.ts +10 -2
  12. package/src/cli-commands/metrics.ts +128 -31
  13. package/src/cli-commands/op.ts +6 -1
  14. package/src/cli-commands/route-args.ts +5 -1
  15. package/src/cli-commands/router.ts +61 -18
  16. package/src/cli-commands/service-daemon.ts +31 -13
  17. package/src/cli-commands/session.ts +15 -4
  18. package/src/cli-commands/support.ts +1 -1
  19. package/src/cli.ts +30 -23
  20. package/src/client.ts +1 -1
  21. package/src/constants.ts +1 -1
  22. package/src/daemon.ts +44 -24
  23. package/src/domain/benchmark.ts +72 -40
  24. package/src/domain/codex-recovery.ts +34 -25
  25. package/src/domain/context-hub.ts +35 -26
  26. package/src/domain/context-telemetry.ts +106 -35
  27. package/src/domain/metric.ts +11 -11
  28. package/src/domain/model-observation.ts +139 -55
  29. package/src/domain/model-ranking-service.ts +13 -3
  30. package/src/domain/model-ranking.ts +126 -60
  31. package/src/domain/task-cost.ts +52 -11
  32. package/src/domain/task-focus.ts +11 -8
  33. package/src/domain/usage.ts +2 -2
  34. package/src/index.ts +69 -69
  35. package/src/log.ts +7 -2
  36. package/src/operations/benchmark-operations.ts +1 -1
  37. package/src/operations/context-operations.ts +15 -6
  38. package/src/operations/metrics-operations.ts +63 -28
  39. package/src/operations/model-ranking-operations.ts +9 -2
  40. package/src/operations/router-operations.ts +8 -4
  41. package/src/operations/session-identity-operations.ts +1 -1
  42. package/src/operations/session-scope.ts +5 -3
  43. package/src/policy.ts +22 -17
  44. package/src/ports/benchmark-controller.ts +1 -5
  45. package/src/ports/metric-store.ts +1 -1
  46. package/src/providers/anthropic-contracts.ts +13 -3
  47. package/src/providers/codex-contracts.ts +60 -52
  48. package/src/providers/codex.ts +16 -19
  49. package/src/providers/google-vertex-budget-contracts.ts +24 -14
  50. package/src/providers/google-vertex-budget.ts +15 -13
  51. package/src/providers/google-vertex-contracts.ts +36 -24
  52. package/src/providers/openrouter-contracts.ts +49 -51
  53. package/src/providers/openrouter.ts +21 -15
  54. package/src/providers/telemetry-sources.ts +13 -10
  55. package/src/router.ts +92 -43
  56. package/src/service.ts +93 -32
  57. package/src/session-identity-service.ts +10 -2
  58. package/src/state.ts +4 -10
  59. package/src/vehicle-registration.ts +154 -0
@@ -8,7 +8,10 @@ import {
8
8
  } from "../constants.ts";
9
9
  import type { MetricObservation, StoredMetricObservation } from "./metric.ts";
10
10
 
11
- interface PayloadSize { characters: number; bytes: number }
11
+ interface PayloadSize {
12
+ characters: number;
13
+ bytes: number;
14
+ }
12
15
 
13
16
  export interface PapyrusContextInjection {
14
17
  schema: typeof PAPYRUS_CONTEXT_INJECTION_SCHEMA;
@@ -26,7 +29,21 @@ export interface PapyrusContextInjection {
26
29
  unchanged: boolean;
27
30
  }
28
31
 
29
- const TOP_LEVEL_FIELDS = new Set(["schema", "observedAt", "sequence", "producerId", "before", "rules", "tasks", "injected", "after", "estimatedTokens", "share", "fingerprint", "unchanged"]);
32
+ const TOP_LEVEL_FIELDS = new Set([
33
+ "schema",
34
+ "observedAt",
35
+ "sequence",
36
+ "producerId",
37
+ "before",
38
+ "rules",
39
+ "tasks",
40
+ "injected",
41
+ "after",
42
+ "estimatedTokens",
43
+ "share",
44
+ "fingerprint",
45
+ "unchanged",
46
+ ]);
30
47
  const SIZE_FIELDS = new Set(["characters", "bytes"]);
31
48
  const RULE_SIZE_FIELDS = new Set(["characters", "bytes", "count"]);
32
49
 
@@ -38,44 +55,46 @@ function record(value: unknown, name: string, fields: Set<string>): Record<strin
38
55
  }
39
56
 
40
57
  function integer(value: unknown, name: string, maximum = Number.MAX_SAFE_INTEGER): number {
41
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > maximum) throw new Error(`${name} must be a bounded non-negative integer`);
58
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > maximum)
59
+ throw new Error(`${name} must be a bounded non-negative integer`);
42
60
  return value;
43
61
  }
44
62
 
45
63
  function size(value: unknown, name: string, fields = SIZE_FIELDS): PayloadSize {
46
64
  const input = record(value, name, fields);
47
65
  return {
48
- characters: integer(input["characters"], `${name}.characters`, CONTEXT_OBSERVATION_MAX_CHARACTERS),
49
- bytes: integer(input["bytes"], `${name}.bytes`, CONTEXT_OBSERVATION_MAX_CHARACTERS * 4),
66
+ characters: integer(input.characters, `${name}.characters`, CONTEXT_OBSERVATION_MAX_CHARACTERS),
67
+ bytes: integer(input.bytes, `${name}.bytes`, CONTEXT_OBSERVATION_MAX_CHARACTERS * 4),
50
68
  };
51
69
  }
52
70
 
53
71
  export function validatePapyrusContextInjection(value: unknown, now = Date.now()): PapyrusContextInjection {
54
72
  const input = record(value, "context injection", TOP_LEVEL_FIELDS);
55
- if (input["schema"] !== PAPYRUS_CONTEXT_INJECTION_SCHEMA) throw new Error("context injection schema is not supported");
56
- const observedAt = integer(input["observedAt"], "observedAt");
73
+ if (input.schema !== PAPYRUS_CONTEXT_INJECTION_SCHEMA) throw new Error("context injection schema is not supported");
74
+ const observedAt = integer(input.observedAt, "observedAt");
57
75
  if (Math.abs(now - observedAt) > CONTEXT_OBSERVATION_MAX_AGE_MS) throw new Error("context injection observation is stale");
58
- const producerId = input["producerId"];
59
- if (typeof producerId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(producerId)) throw new Error("producerId must be a UUID");
60
- const before = size(input["before"], "before");
61
- const rulesInput = record(input["rules"], "rules", RULE_SIZE_FIELDS);
62
- const rules = { ...size(rulesInput, "rules", RULE_SIZE_FIELDS), count: integer(rulesInput["count"], "rules.count") };
63
- const tasks = size(input["tasks"], "tasks");
64
- const injected = size(input["injected"], "injected");
65
- const after = size(input["after"], "after");
76
+ const producerId = input.producerId;
77
+ if (typeof producerId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(producerId))
78
+ throw new Error("producerId must be a UUID");
79
+ const before = size(input.before, "before");
80
+ const rulesInput = record(input.rules, "rules", RULE_SIZE_FIELDS);
81
+ const rules = { ...size(rulesInput, "rules", RULE_SIZE_FIELDS), count: integer(rulesInput.count, "rules.count") };
82
+ const tasks = size(input.tasks, "tasks");
83
+ const injected = size(input.injected, "injected");
84
+ const after = size(input.after, "after");
66
85
  if (injected.characters !== rules.characters + tasks.characters || after.characters !== before.characters + injected.characters) {
67
86
  throw new Error("context injection sizes are inconsistent");
68
87
  }
69
- const estimatedTokens = integer(input["estimatedTokens"], "estimatedTokens", CONTEXT_OBSERVATION_MAX_CHARACTERS);
70
- const share = input["share"];
88
+ const estimatedTokens = integer(input.estimatedTokens, "estimatedTokens", CONTEXT_OBSERVATION_MAX_CHARACTERS);
89
+ const share = input.share;
71
90
  if (typeof share !== "number" || !Number.isFinite(share) || share < 0 || share > 1) throw new Error("share must be a ratio");
72
- const fingerprint = input["fingerprint"];
91
+ const fingerprint = input.fingerprint;
73
92
  if (typeof fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(fingerprint)) throw new Error("fingerprint must be a SHA-256 hex digest");
74
- if (typeof input["unchanged"] !== "boolean") throw new Error("unchanged must be boolean");
93
+ if (typeof input.unchanged !== "boolean") throw new Error("unchanged must be boolean");
75
94
  return {
76
95
  schema: PAPYRUS_CONTEXT_INJECTION_SCHEMA,
77
96
  observedAt,
78
- sequence: integer(input["sequence"], "sequence"),
97
+ sequence: integer(input.sequence, "sequence"),
79
98
  producerId,
80
99
  before,
81
100
  rules,
@@ -85,7 +104,7 @@ export function validatePapyrusContextInjection(value: unknown, now = Date.now()
85
104
  estimatedTokens,
86
105
  share,
87
106
  fingerprint,
88
- unchanged: input["unchanged"],
107
+ unchanged: input.unchanged,
89
108
  };
90
109
  }
91
110
 
@@ -121,7 +140,9 @@ export interface CompactionStart {
121
140
  contextTokens?: number;
122
141
  }
123
142
 
124
- interface OpenCompaction extends CompactionStart { startedAt: number }
143
+ interface OpenCompaction extends CompactionStart {
144
+ startedAt: number;
145
+ }
125
146
  interface UsageCounters {
126
147
  turns: number;
127
148
  injectedCharacters: number;
@@ -140,8 +161,12 @@ export class CompactionTelemetry {
140
161
  private counters = emptyCounters();
141
162
  private previousCompletedAt: number | undefined;
142
163
 
143
- hasOpenCompaction(): boolean { return this.open !== undefined; }
144
- observeTurn(): void { this.counters.turns += 1; }
164
+ hasOpenCompaction(): boolean {
165
+ return this.open !== undefined;
166
+ }
167
+ observeTurn(): void {
168
+ this.counters.turns += 1;
169
+ }
145
170
  observeInjection(characters: number, estimatedTokens: number): void {
146
171
  this.counters.injectedCharacters += Math.max(0, characters);
147
172
  this.counters.estimatedInjectedTokens += Math.max(0, estimatedTokens);
@@ -154,26 +179,68 @@ export class CompactionTelemetry {
154
179
 
155
180
  begin(input: CompactionStart, now = Date.now()): MetricObservation {
156
181
  this.open = { ...input, startedAt: now };
157
- return { source: "pi-context", scope: "compaction", metric: "compaction-started", value: 1, unit: "count", observedAt: now, attributes: { ...input } };
182
+ return {
183
+ source: "pi-context",
184
+ scope: "compaction",
185
+ metric: "compaction-started",
186
+ value: 1,
187
+ unit: "count",
188
+ observedAt: now,
189
+ attributes: { ...input },
190
+ };
158
191
  }
159
192
 
160
193
  complete(input: Pick<CompactionStart, "reason" | "willRetry">, now = Date.now()): MetricObservation {
161
- if (!this.open) return { source: "pi-context", scope: "compaction", metric: "compaction-unmatched", value: 1, unit: "count", observedAt: now, attributes: { ...input } };
194
+ if (!this.open)
195
+ return {
196
+ source: "pi-context",
197
+ scope: "compaction",
198
+ metric: "compaction-unmatched",
199
+ value: 1,
200
+ unit: "count",
201
+ observedAt: now,
202
+ attributes: { ...input },
203
+ };
162
204
  const open = this.open;
163
205
  this.open = undefined;
164
206
  const attributes = this.intervalAttributes(open, now);
165
207
  this.previousCompletedAt = now;
166
208
  this.counters = emptyCounters();
167
- return { source: "pi-context", scope: "compaction", metric: "compaction-duration", value: Math.max(0, now - open.startedAt), unit: "milliseconds", observedAt: now, attributes: { ...attributes, reason: input.reason, willRetry: input.willRetry } };
209
+ return {
210
+ source: "pi-context",
211
+ scope: "compaction",
212
+ metric: "compaction-duration",
213
+ value: Math.max(0, now - open.startedAt),
214
+ unit: "milliseconds",
215
+ observedAt: now,
216
+ attributes: { ...attributes, reason: input.reason, willRetry: input.willRetry },
217
+ };
168
218
  }
169
219
 
170
220
  abort(now = Date.now(), abortReason = "aborted"): MetricObservation {
171
221
  const open = this.open;
172
222
  this.open = undefined;
173
- if (!open) return { source: "pi-context", scope: "compaction", metric: "compaction-unmatched", value: 1, unit: "count", observedAt: now, attributes: { abortReason } };
223
+ if (!open)
224
+ return {
225
+ source: "pi-context",
226
+ scope: "compaction",
227
+ metric: "compaction-unmatched",
228
+ value: 1,
229
+ unit: "count",
230
+ observedAt: now,
231
+ attributes: { abortReason },
232
+ };
174
233
  const attributes = this.intervalAttributes(open, now);
175
234
  this.counters = emptyCounters();
176
- return { source: "pi-context", scope: "compaction", metric: "compaction-aborted", value: 1, unit: "count", observedAt: now, attributes: { ...attributes, reason: open.reason, abortReason, durationMs: Math.max(0, now - open.startedAt) } };
235
+ return {
236
+ source: "pi-context",
237
+ scope: "compaction",
238
+ metric: "compaction-aborted",
239
+ value: 1,
240
+ unit: "count",
241
+ observedAt: now,
242
+ attributes: { ...attributes, reason: open.reason, abortReason, durationMs: Math.max(0, now - open.startedAt) },
243
+ };
177
244
  }
178
245
 
179
246
  private intervalAttributes(open: OpenCompaction, now: number): Record<string, unknown> {
@@ -226,8 +293,12 @@ function numericAttribute(row: StoredMetricObservation, key: string): number | n
226
293
  const value = row.attributes[key];
227
294
  return typeof value === "number" && Number.isFinite(value) ? value : null;
228
295
  }
229
- function average(values: number[]): number | null { return values.length === 0 ? null : values.reduce((sum, value) => sum + value, 0) / values.length; }
230
- function sum(values: number[]): number { return values.reduce((total, value) => total + value, 0); }
296
+ function average(values: number[]): number | null {
297
+ return values.length === 0 ? null : values.reduce((sum, value) => sum + value, 0) / values.length;
298
+ }
299
+ function sum(values: number[]): number {
300
+ return values.reduce((total, value) => total + value, 0);
301
+ }
231
302
  function percentile(values: number[], percentage: number): number | null {
232
303
  if (values.length === 0) return null;
233
304
  const sorted = [...values].sort((left, right) => left - right);
@@ -239,14 +310,14 @@ export function assessContextTelemetry(
239
310
  compactions: StoredMetricObservation[],
240
311
  options: { since: number; until: number; truncated: boolean },
241
312
  ): ContextAssessment {
242
- const injectionValues = injections.flatMap((row) => typeof row.value === "number" ? [row.value] : []);
313
+ const injectionValues = injections.flatMap((row) => (typeof row.value === "number" ? [row.value] : []));
243
314
  const shares = injections.flatMap((row) => numericAttribute(row, "share") ?? []);
244
- const unchanged = injections.filter((row) => row.attributes["unchanged"] === true).length;
315
+ const unchanged = injections.filter((row) => row.attributes.unchanged === true).length;
245
316
  const completed = compactions.filter((row) => row.metric === "compaction-duration" && typeof row.value === "number");
246
317
  const aborted = compactions.filter((row) => row.metric === "compaction-aborted");
247
318
  const reasons = { manual: 0, threshold: 0, overflow: 0 };
248
319
  for (const row of completed) {
249
- const reason = row.attributes["reason"];
320
+ const reason = row.attributes.reason;
250
321
  if (reason === "manual" || reason === "threshold" || reason === "overflow") reasons[reason] += 1;
251
322
  }
252
323
  const windowMs = Math.max(0, options.until - options.since);
@@ -302,7 +373,7 @@ export function estimateCompactionDuration(compactions: StoredMetricObservation[
302
373
  .filter((row) => row.source === "pi-context" && row.scope === "compaction" && row.metric === "compaction-duration")
303
374
  .sort((left, right) => right.observedAt - left.observedAt || right.id - left.id)
304
375
  .slice(0, COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES)
305
- .flatMap((row) => typeof row.value === "number" && Number.isFinite(row.value) && row.value >= 0 ? [row.value] : []);
376
+ .flatMap((row) => (typeof row.value === "number" && Number.isFinite(row.value) && row.value >= 0 ? [row.value] : []));
306
377
  if (durations.length < COMPACTION_DURATION_ESTIMATE_MIN_SAMPLES) {
307
378
  return { ms: null, confidence: "cold-start", sampleSize: durations.length, observedAt: now };
308
379
  }
@@ -1,7 +1,7 @@
1
1
  import { METRIC_ATTRIBUTES_MAX_DEPTH, METRIC_ATTRIBUTES_MAX_SERIALIZED_CHARACTERS, METRIC_IDENTITY_MAX_CHARACTERS } from "../constants.ts";
2
2
 
3
3
  export const METRIC_UNITS = ["ratio", "usd", "tokens", "tokens-per-second", "requests", "milliseconds", "count", "elo"] as const;
4
- export type MetricUnit = typeof METRIC_UNITS[number];
4
+ export type MetricUnit = (typeof METRIC_UNITS)[number];
5
5
 
6
6
  export interface MetricObservation {
7
7
  source: string;
@@ -75,21 +75,21 @@ export function validateMetricObservation(value: unknown): MetricObservation {
75
75
  if (typeof input[key] !== "string" || input[key].trim().length === 0) throw new Error(`${key} is required`);
76
76
  if (input[key].length > METRIC_IDENTITY_MAX_CHARACTERS) throw new Error(`${key} exceeds the length limit`);
77
77
  }
78
- if (!METRIC_UNITS.includes(input["unit"] as MetricUnit)) throw new Error("unit is not supported");
79
- if (input["value"] !== null && (typeof input["value"] !== "number" || !Number.isFinite(input["value"]))) {
78
+ if (!METRIC_UNITS.includes(input.unit as MetricUnit)) throw new Error("unit is not supported");
79
+ if (input.value !== null && (typeof input.value !== "number" || !Number.isFinite(input.value))) {
80
80
  throw new Error("value must be finite or null");
81
81
  }
82
- if (typeof input["observedAt"] !== "number" || !Number.isSafeInteger(input["observedAt"]) || input["observedAt"] < 0) {
82
+ if (typeof input.observedAt !== "number" || !Number.isSafeInteger(input.observedAt) || input.observedAt < 0) {
83
83
  throw new Error("observedAt must be a non-negative integer timestamp");
84
84
  }
85
- const attributes = validateAttributes(input["attributes"]);
85
+ const attributes = validateAttributes(input.attributes);
86
86
  return {
87
- source: input["source"] as string,
88
- scope: input["scope"] as string,
89
- metric: input["metric"] as string,
90
- value: input["value"] as number | null,
91
- unit: input["unit"] as MetricUnit,
92
- observedAt: input["observedAt"] as number,
87
+ source: input.source as string,
88
+ scope: input.scope as string,
89
+ metric: input.metric as string,
90
+ value: input.value as number | null,
91
+ unit: input.unit as MetricUnit,
92
+ observedAt: input.observedAt as number,
93
93
  attributes,
94
94
  };
95
95
  }
@@ -17,9 +17,9 @@ import type { MetricObservation, MetricUnit, StoredMetricObservation } from "./m
17
17
  * research on type simultaneously (e.g. reading a file, then searching the web in one turn).
18
18
  */
19
19
  export const TASK_DOMAINS = ["coding", "design", "math", "general"] as const;
20
- export type ModelTaskDomain = typeof TASK_DOMAINS[number];
20
+ export type ModelTaskDomain = (typeof TASK_DOMAINS)[number];
21
21
  export const TASK_TYPES = ["research", "planning", "general"] as const;
22
- export type ModelTaskType = typeof TASK_TYPES[number];
22
+ export type ModelTaskType = (typeof TASK_TYPES)[number];
23
23
  export type ExplicitOutcome = "accepted" | "rejected" | "unknown";
24
24
 
25
25
  export interface ModelRunObservation {
@@ -67,50 +67,80 @@ export interface ModelAggregateOptions {
67
67
  }
68
68
 
69
69
  const ALLOWED_FIELDS = new Set<keyof ModelRunObservation>([
70
- "runId", "provider", "model", "thinking", "domain", "type", "startedAt", "firstTokenAt", "completedAt",
71
- "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "costUsd", "providerResponses",
72
- "toolCalls", "toolFailures", "stopReason", "explicitOutcome",
70
+ "runId",
71
+ "provider",
72
+ "model",
73
+ "thinking",
74
+ "domain",
75
+ "type",
76
+ "startedAt",
77
+ "firstTokenAt",
78
+ "completedAt",
79
+ "inputTokens",
80
+ "outputTokens",
81
+ "cacheReadTokens",
82
+ "cacheWriteTokens",
83
+ "costUsd",
84
+ "providerResponses",
85
+ "toolCalls",
86
+ "toolFailures",
87
+ "stopReason",
88
+ "explicitOutcome",
73
89
  ]);
74
90
  const STOP_REASONS = new Set<ModelRunObservation["stopReason"]>(["stop", "length", "toolUse", "error", "aborted", "unknown"]);
75
91
  const OUTCOMES = new Set<ExplicitOutcome>(["accepted", "rejected", "unknown"]);
76
92
 
77
93
  function text(value: unknown, name: string): string {
78
- if (typeof value !== "string" || value.length === 0 || value.length > MODEL_OBSERVATION_IDENTITY_MAX_CHARACTERS || /\p{Cc}/u.test(value)) throw new Error(`${name} is invalid`);
94
+ if (typeof value !== "string" || value.length === 0 || value.length > MODEL_OBSERVATION_IDENTITY_MAX_CHARACTERS || /\p{Cc}/u.test(value))
95
+ throw new Error(`${name} is invalid`);
79
96
  return value;
80
97
  }
81
98
 
82
99
  function nonNegative(value: unknown, name: string, integer = false): number {
83
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || (integer && !Number.isSafeInteger(value))) throw new Error(`${name} is invalid`);
100
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || (integer && !Number.isSafeInteger(value)))
101
+ throw new Error(`${name} is invalid`);
84
102
  return value;
85
103
  }
86
104
 
87
105
  export function validateModelRunObservation(value: unknown): ModelRunObservation {
88
106
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("model run observation must be an object");
89
107
  const input = value as Record<string, unknown>;
90
- for (const key of Object.keys(input)) if (!ALLOWED_FIELDS.has(key as keyof ModelRunObservation)) throw new Error(`unsupported field: ${key}`);
91
- const identity = normalizeModelIdentity(text(input["provider"], "provider"), text(input["model"], "model"));
92
- const startedAt = nonNegative(input["startedAt"], "start time", true);
93
- const completedAt = nonNegative(input["completedAt"], "completion time", true);
94
- const firstTokenAt = input["firstTokenAt"] === null ? null : nonNegative(input["firstTokenAt"], "first-token time", true);
95
- if (completedAt < startedAt || (firstTokenAt !== null && (firstTokenAt < startedAt || firstTokenAt > completedAt))) throw new Error("model run timestamps are not ordered");
96
- if (!TASK_DOMAINS.includes(input["domain"] as ModelTaskDomain)) throw new Error("task domain is invalid");
97
- if (!TASK_TYPES.includes(input["type"] as ModelTaskType)) throw new Error("task type is invalid");
98
- if (!STOP_REASONS.has(input["stopReason"] as ModelRunObservation["stopReason"])) throw new Error("stop reason is invalid");
99
- if (!OUTCOMES.has(input["explicitOutcome"] as ExplicitOutcome)) throw new Error("explicit outcome is invalid");
100
- const providerResponses = nonNegative(input["providerResponses"], "provider response count", true);
101
- const toolCalls = nonNegative(input["toolCalls"], "tool call count", true);
102
- const toolFailures = nonNegative(input["toolFailures"], "tool failure count", true);
108
+ for (const key of Object.keys(input))
109
+ if (!ALLOWED_FIELDS.has(key as keyof ModelRunObservation)) throw new Error(`unsupported field: ${key}`);
110
+ const identity = normalizeModelIdentity(text(input.provider, "provider"), text(input.model, "model"));
111
+ const startedAt = nonNegative(input.startedAt, "start time", true);
112
+ const completedAt = nonNegative(input.completedAt, "completion time", true);
113
+ const firstTokenAt = input.firstTokenAt === null ? null : nonNegative(input.firstTokenAt, "first-token time", true);
114
+ if (completedAt < startedAt || (firstTokenAt !== null && (firstTokenAt < startedAt || firstTokenAt > completedAt)))
115
+ throw new Error("model run timestamps are not ordered");
116
+ if (!TASK_DOMAINS.includes(input.domain as ModelTaskDomain)) throw new Error("task domain is invalid");
117
+ if (!TASK_TYPES.includes(input.type as ModelTaskType)) throw new Error("task type is invalid");
118
+ if (!STOP_REASONS.has(input.stopReason as ModelRunObservation["stopReason"])) throw new Error("stop reason is invalid");
119
+ if (!OUTCOMES.has(input.explicitOutcome as ExplicitOutcome)) throw new Error("explicit outcome is invalid");
120
+ const providerResponses = nonNegative(input.providerResponses, "provider response count", true);
121
+ const toolCalls = nonNegative(input.toolCalls, "tool call count", true);
122
+ const toolFailures = nonNegative(input.toolFailures, "tool failure count", true);
103
123
  if (providerResponses < 1 || toolFailures > toolCalls) throw new Error("model run counters are inconsistent");
104
124
  return {
105
- runId: text(input["runId"], "run id"), provider: identity.provider, model: identity.model,
106
- thinking: text(input["thinking"], "thinking level"), domain: input["domain"] as ModelTaskDomain, type: input["type"] as ModelTaskType,
107
- startedAt, firstTokenAt, completedAt,
108
- inputTokens: nonNegative(input["inputTokens"], "input tokens"),
109
- outputTokens: nonNegative(input["outputTokens"], "output tokens"),
110
- cacheReadTokens: nonNegative(input["cacheReadTokens"], "cache read tokens"),
111
- cacheWriteTokens: nonNegative(input["cacheWriteTokens"], "cache write tokens"),
112
- costUsd: nonNegative(input["costUsd"], "cost"), providerResponses, toolCalls, toolFailures,
113
- stopReason: input["stopReason"] as ModelRunObservation["stopReason"], explicitOutcome: input["explicitOutcome"] as ExplicitOutcome,
125
+ runId: text(input.runId, "run id"),
126
+ provider: identity.provider,
127
+ model: identity.model,
128
+ thinking: text(input.thinking, "thinking level"),
129
+ domain: input.domain as ModelTaskDomain,
130
+ type: input.type as ModelTaskType,
131
+ startedAt,
132
+ firstTokenAt,
133
+ 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"),
138
+ costUsd: nonNegative(input.costUsd, "cost"),
139
+ providerResponses,
140
+ toolCalls,
141
+ toolFailures,
142
+ stopReason: input.stopReason as ModelRunObservation["stopReason"],
143
+ explicitOutcome: input.explicitOutcome as ExplicitOutcome,
114
144
  };
115
145
  }
116
146
 
@@ -122,18 +152,37 @@ export interface ModelTaskClassification {
122
152
  /** Domain and type are independent: a run can be domain=coding and type=research at once (e.g. reading a file, then searching the web in the same turn). */
123
153
  export function classifyTaskFromTools(toolNames: string[]): ModelTaskClassification {
124
154
  const names = new Set(toolNames.slice(0, 100).map((name) => name.toLowerCase()));
125
- const domain: ModelTaskDomain = ["edit", "write", "read", "bash", "grep", "find", "ls"].some((name) => names.has(name)) ? "coding" : "general";
155
+ const domain: ModelTaskDomain = ["edit", "write", "read", "bash", "grep", "find", "ls"].some((name) => names.has(name))
156
+ ? "coding"
157
+ : "general";
126
158
  const type: ModelTaskType = ["web_fetch", "web_search"].some((name) => names.has(name))
127
159
  ? "research"
128
- : ["tasks", "papyrus_create", "papyrus_graph"].some((name) => names.has(name)) ? "planning" : "general";
160
+ : ["tasks", "papyrus_create", "papyrus_graph"].some((name) => names.has(name))
161
+ ? "planning"
162
+ : "general";
129
163
  return { domain, type };
130
164
  }
131
165
 
132
166
  export function modelRunMetrics(value: ModelRunObservation): MetricObservation[] {
133
167
  const run = validateModelRunObservation(value);
134
168
  const scope = `${run.provider}/${run.model}`;
135
- const attributes = { provider: run.provider, model: run.model, thinking: run.thinking, domain: run.domain, type: run.type, runId: run.runId };
136
- const metric = (name: string, amount: number, unit: MetricUnit): MetricObservation => ({ source: "local-model", scope, metric: name, value: amount, unit, observedAt: run.completedAt, attributes });
169
+ const attributes = {
170
+ provider: run.provider,
171
+ model: run.model,
172
+ thinking: run.thinking,
173
+ domain: run.domain,
174
+ type: run.type,
175
+ runId: run.runId,
176
+ };
177
+ const metric = (name: string, amount: number, unit: MetricUnit): MetricObservation => ({
178
+ source: "local-model",
179
+ scope,
180
+ metric: name,
181
+ value: amount,
182
+ unit,
183
+ observedAt: run.completedAt,
184
+ attributes,
185
+ });
137
186
  const wallMs = run.completedAt - run.startedAt;
138
187
  const totalInput = run.inputTokens + run.cacheReadTokens;
139
188
  const metrics: MetricObservation[] = [];
@@ -169,35 +218,70 @@ function median(sorted: number[]): number {
169
218
  export function aggregateModelMetrics(input: StoredMetricObservation[], options: ModelAggregateOptions = {}): ModelMetricAggregate[] {
170
219
  const now = options.now ?? Date.now();
171
220
  const freshForMs = options.freshForMs ?? MODEL_OBSERVATION_FRESH_MS;
172
- if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(freshForMs) || freshForMs <= 0) throw new Error("aggregate time bounds are invalid");
221
+ if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(freshForMs) || freshForMs <= 0)
222
+ throw new Error("aggregate time bounds are invalid");
173
223
  const groups = new Map<string, StoredMetricObservation[]>();
174
224
  for (const row of input.slice(0, MODEL_AGGREGATE_MAX_ROWS)) {
175
225
  if (row.source !== "local-model" || typeof row.value !== "number" || !Number.isFinite(row.value)) continue;
176
- const provider = row.attributes["provider"];
177
- const model = row.attributes["model"];
178
- const thinking = row.attributes["thinking"];
179
- const domain = row.attributes["domain"];
180
- const type = row.attributes["type"];
181
- if (typeof provider !== "string" || typeof model !== "string" || typeof thinking !== "string" || !TASK_DOMAINS.includes(domain as ModelTaskDomain) || !TASK_TYPES.includes(type as ModelTaskType)) continue;
226
+ const provider = row.attributes.provider;
227
+ const model = row.attributes.model;
228
+ const thinking = row.attributes.thinking;
229
+ const domain = row.attributes.domain;
230
+ const type = row.attributes.type;
231
+ if (
232
+ typeof provider !== "string" ||
233
+ typeof model !== "string" ||
234
+ typeof thinking !== "string" ||
235
+ !TASK_DOMAINS.includes(domain as ModelTaskDomain) ||
236
+ !TASK_TYPES.includes(type as ModelTaskType)
237
+ )
238
+ continue;
182
239
  const key = JSON.stringify([provider, model, thinking, domain, type, row.metric, row.unit]);
183
240
  if (!groups.has(key) && groups.size >= MODEL_AGGREGATE_MAX_GROUPS) continue;
184
241
  const rows = groups.get(key) ?? [];
185
242
  rows.push(row);
186
243
  groups.set(key, rows);
187
244
  }
188
- return [...groups.entries()].map(([key, rows]) => {
189
- const [provider, model, thinking, domain, type, dimension, unit] = JSON.parse(key) as [string, string, string, ModelTaskDomain, ModelTaskType, string, MetricUnit];
190
- const values = rows.map((row) => row.value as number).sort((left, right) => left - right);
191
- const center = median(values);
192
- const deviations = values.map((value) => Math.abs(value - center)).sort((left, right) => left - right);
193
- const latestAt = Math.max(...rows.map((row) => row.observedAt));
194
- const age = Math.max(0, now - latestAt);
195
- const recency = Math.max(0, 1 - (age / freshForMs));
196
- return {
197
- provider, model, thinking, domain, type, dimension, unit,
198
- sampleSize: values.length, median: center, p90: percentile(values, 0.9), medianAbsoluteDeviation: median(deviations), latestAt,
199
- freshness: age <= freshForMs ? "fresh" as const : "stale" as const,
200
- confidence: Math.min(1, Math.sqrt(values.length / 20)) * recency,
201
- };
202
- }).sort((left, right) => left.provider.localeCompare(right.provider) || left.model.localeCompare(right.model) || left.domain.localeCompare(right.domain) || left.type.localeCompare(right.type) || left.dimension.localeCompare(right.dimension));
245
+ return [...groups.entries()]
246
+ .map(([key, rows]) => {
247
+ const [provider, model, thinking, domain, type, dimension, unit] = JSON.parse(key) as [
248
+ string,
249
+ string,
250
+ string,
251
+ ModelTaskDomain,
252
+ ModelTaskType,
253
+ string,
254
+ MetricUnit,
255
+ ];
256
+ const values = rows.map((row) => row.value as number).sort((left, right) => left - right);
257
+ const center = median(values);
258
+ const deviations = values.map((value) => Math.abs(value - center)).sort((left, right) => left - right);
259
+ const latestAt = Math.max(...rows.map((row) => row.observedAt));
260
+ const age = Math.max(0, now - latestAt);
261
+ const recency = Math.max(0, 1 - age / freshForMs);
262
+ return {
263
+ provider,
264
+ model,
265
+ thinking,
266
+ domain,
267
+ type,
268
+ dimension,
269
+ unit,
270
+ sampleSize: values.length,
271
+ median: center,
272
+ p90: percentile(values, 0.9),
273
+ medianAbsoluteDeviation: median(deviations),
274
+ latestAt,
275
+ freshness: age <= freshForMs ? ("fresh" as const) : ("stale" as const),
276
+ confidence: Math.min(1, Math.sqrt(values.length / 20)) * recency,
277
+ };
278
+ })
279
+ .sort(
280
+ (left, right) =>
281
+ left.provider.localeCompare(right.provider) ||
282
+ left.model.localeCompare(right.model) ||
283
+ left.domain.localeCompare(right.domain) ||
284
+ left.type.localeCompare(right.type) ||
285
+ left.dimension.localeCompare(right.dimension),
286
+ );
203
287
  }
@@ -1,9 +1,15 @@
1
1
  import { MODEL_AGGREGATE_MAX_ROWS, MODEL_OBSERVATION_FRESH_MS, MODEL_RANKING_MAX_SOURCES } from "../constants.ts";
2
2
  import type { BenchmarkStore } from "../ports/benchmark-store.ts";
3
3
  import type { MetricStore } from "../ports/metric-store.ts";
4
- import { aggregateModelMetrics } from "./model-observation.ts";
5
- import { rankModelCandidates, type ModelCandidate, type ModelRankingResult, type ScopeAuthority, type UtilityWeights } from "./model-ranking.ts";
6
4
  import type { ModelTaskDomain, ModelTaskType } from "./model-observation.ts";
5
+ import { aggregateModelMetrics } from "./model-observation.ts";
6
+ import {
7
+ type ModelCandidate,
8
+ type ModelRankingResult,
9
+ rankModelCandidates,
10
+ type ScopeAuthority,
11
+ type UtilityWeights,
12
+ } from "./model-ranking.ts";
7
13
 
8
14
  export interface ModelRecommendationInput {
9
15
  candidates: ModelCandidate[];
@@ -27,7 +33,11 @@ export class EvidenceModelRanker implements ModelRanker {
27
33
  ) {}
28
34
 
29
35
  rank(input: ModelRecommendationInput): ModelRankingResult {
30
- if (!Array.isArray(input.sourceIds) || input.sourceIds.length > MODEL_RANKING_MAX_SOURCES || !input.sourceIds.every((sourceId) => typeof sourceId === "string" && sourceId.length > 0 && sourceId.length <= 160)) {
36
+ if (
37
+ !Array.isArray(input.sourceIds) ||
38
+ input.sourceIds.length > MODEL_RANKING_MAX_SOURCES ||
39
+ !input.sourceIds.every((sourceId) => typeof sourceId === "string" && sourceId.length > 0 && sourceId.length <= 160)
40
+ ) {
31
41
  throw new Error("benchmark source selection is invalid");
32
42
  }
33
43
  const sourceIds = [...new Set(input.sourceIds)];