@danypops/jittor 0.5.0 → 0.6.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.
@@ -1,28 +1,65 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
- import { USAGE_CHART_HEIGHT, USAGE_TOKEN_QUERY_LIMIT, USAGE_Y_AXIS_WIDTH } from "../../src/constants.ts";
3
+ import { HUMAN_TEXT_FIELD_MAX_CHARACTERS, USAGE_CHART_HEIGHT, USAGE_RENDER_MAX_SERIES, USAGE_TOKEN_QUERY_LIMIT, USAGE_Y_AXIS_WIDTH } from "../../src/constants.ts";
4
4
  import type { StoredMetricObservation } from "../../src/domain/metric.ts";
5
5
  import {
6
+ buildCostGraph,
6
7
  buildUsageGraph,
7
8
  USAGE_PERIODS,
8
9
  usagePeriod,
9
10
  usagePeriodStart,
10
- type UsageBucket,
11
+ type CostGraph,
11
12
  type UsageGraph,
12
13
  type UsagePeriod,
13
14
  } from "../../src/domain/usage.ts";
14
15
  import type { UsageBudgetControl } from "./settings.ts";
15
16
  import type { JittorPanelClient } from "./tui.ts";
16
17
 
17
- type UsageAction = "period-prev" | "period-next" | "refresh" | "close";
18
- type UsageColor = "accent" | "success" | "warning" | "error" | "thinkingText" | "muted" | "dim" | "borderMuted";
18
+ type UsageAction = "period-prev" | "period-next" | "view-next" | "refresh" | "close";
19
+ type UsageColor =
20
+ | "accent" | "success" | "warning" | "error" | "thinkingText" | "muted" | "dim" | "borderMuted"
21
+ | "syntaxKeyword" | "syntaxFunction" | "syntaxVariable" | "syntaxString" | "syntaxNumber" | "syntaxType" | "syntaxOperator";
19
22
 
20
23
  export interface UsageTheme {
21
24
  fg(color: UsageColor, text: string): string;
22
25
  bold(text: string): string;
23
26
  }
24
27
 
25
- const SERIES_COLORS: UsageColor[] = ["accent", "success", "warning", "thinkingText", "error"];
28
+ /**
29
+ * Categorical palette for per-provider/model series. Deliberately excludes "success"/"warning"/
30
+ * "error": those already carry a fixed status meaning elsewhere in this same panel (the budget
31
+ * threshold line, freshness state), so reusing them for arbitrary model identity would make a
32
+ * model's bar segment look like a warning or a failure to a pre-attentive reader — a real
33
+ * categorical-color-design pitfall (see e.g. ColorBrewer/Okabe-Ito guidance on qualitative
34
+ * palettes: colors for nominal categories should not imply an order, magnitude, or judgment).
35
+ *
36
+ * Instead this reuses the syntax-highlighting color roles, because theme authors already tune
37
+ * those specifically to be simultaneously distinguishable on screen — that is the same design
38
+ * problem as a categorical data palette (many hues coexisting in one view that all need to read
39
+ * as different from each other). The order below interleaves the hue families syntax themes
40
+ * conventionally assign to keyword/function/string/number/type/variable/operator (violet, blue,
41
+ * green, orange, teal, cyan, neutral) so the first colors used are spread around the hue wheel
42
+ * rather than clustered, which is the standard categorical-palette heuristic for maximizing
43
+ * perceptual separation between adjacent categories.
44
+ *
45
+ * Terminal foreground color is a single channel with a hard ceiling on how many hues stay mutually
46
+ * distinguishable (most qualitative-palette guidance caps around 8–12). Once the hue palette is
47
+ * exhausted, seriesStyle adds bold as a second, independent visual channel before any exact
48
+ * color+weight combination repeats — a standard visualization technique (encode extra categories
49
+ * on an additional channel rather than silently reusing an indistinguishable color).
50
+ */
51
+ const SERIES_HUES: UsageColor[] = [
52
+ "accent", "syntaxFunction", "syntaxString", "syntaxNumber",
53
+ "syntaxKeyword", "syntaxType", "thinkingText", "syntaxVariable", "syntaxOperator",
54
+ ];
55
+
56
+ /** Returns a style function for the Nth series: cycles hue, then adds bold once the palette wraps. */
57
+ function seriesStyle(index: number, theme: UsageTheme): (text: string) => string {
58
+ const hue = SERIES_HUES[index % SERIES_HUES.length]!;
59
+ const useBold = Math.floor(index / SERIES_HUES.length) % 2 === 1;
60
+ return (text: string) => useBold ? theme.bold(theme.fg(hue, text)) : theme.fg(hue, text);
61
+ }
62
+
26
63
  const PARTIAL_BLOCKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
27
64
 
28
65
  function compact(value: number): string {
@@ -32,9 +69,13 @@ function compact(value: number): string {
32
69
  return String(Math.round(value));
33
70
  }
34
71
 
35
- function mergeBuckets(buckets: UsageBucket[], maximum: number): UsageBucket[] {
72
+ interface RenderableSeries { key: string; provider: string; model: string; total: number }
73
+ interface RenderableBucket { start: number; end: number; total: number; series: Record<string, number> }
74
+ interface RenderableChart { period: UsagePeriod; start: number; end: number; buckets: RenderableBucket[]; series: RenderableSeries[]; total: number; truncated: boolean }
75
+
76
+ function mergeBuckets(buckets: RenderableBucket[], maximum: number): RenderableBucket[] {
36
77
  if (buckets.length <= maximum) return buckets;
37
- const result: UsageBucket[] = [];
78
+ const result: RenderableBucket[] = [];
38
79
  for (let index = 0; index < maximum; index += 1) {
39
80
  const from = Math.floor(index * buckets.length / maximum);
40
81
  const to = Math.max(from + 1, Math.floor((index + 1) * buckets.length / maximum));
@@ -53,15 +94,24 @@ function mergeBuckets(buckets: UsageBucket[], maximum: number): UsageBucket[] {
53
94
  return result;
54
95
  }
55
96
 
56
- function seriesAt(bucket: UsageBucket, chart: UsageGraph, tokenHeight: number): number {
97
+ function seriesAt(bucket: RenderableBucket, chart: RenderableChart, valueHeight: number): number {
57
98
  let cumulative = 0;
58
99
  for (let index = 0; index < chart.series.length; index += 1) {
59
100
  cumulative += bucket.series[chart.series[index]!.key] ?? 0;
60
- if (tokenHeight <= cumulative) return index;
101
+ if (valueHeight <= cumulative) return index;
61
102
  }
62
103
  return Math.max(0, chart.series.length - 1);
63
104
  }
64
105
 
106
+ /** Compact USD formatter: full cents below $1k, then the same k/M suffix convention as compact(). */
107
+ function formatUsd(value: number): string {
108
+ const magnitude = Math.abs(value);
109
+ if (magnitude === 0) return "$0";
110
+ if (magnitude < 1_000) return `$${value.toFixed(magnitude < 0.01 ? 4 : 2)}`;
111
+ if (magnitude < 1_000_000) return `$${(value / 1_000).toFixed(1)}k`;
112
+ return `$${(value / 1_000_000).toFixed(1)}M`;
113
+ }
114
+
65
115
  function formatPeriodPoint(value: number, period: UsagePeriod): string {
66
116
  const date = new Date(value);
67
117
  if (period === "hourly" || period === "daily") return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
@@ -80,11 +130,27 @@ function axisLabels(start: number, end: number, period: UsagePeriod, width: numb
80
130
  return characters.join("");
81
131
  }
82
132
 
133
+ function displayIdentity(value: string): string {
134
+ return value.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim().slice(0, HUMAN_TEXT_FIELD_MAX_CHARACTERS);
135
+ }
136
+
83
137
  function plainTheme(): UsageTheme {
84
138
  return { fg: (_color, text) => text, bold: (text) => text };
85
139
  }
86
140
 
87
- export function renderUsageGraph(chart: UsageGraph, width: number, theme: UsageTheme, tokenBudget?: number): string[] {
141
+ interface ChartRenderOptions {
142
+ title: string;
143
+ formatValue: (value: number) => string;
144
+ /** Appended after the observed/budget amount, e.g. " tokens"; empty when formatValue already carries a unit prefix like "$". */
145
+ unitSuffix: string;
146
+ subtitle?: string;
147
+ budget?: number;
148
+ noDataText: string;
149
+ }
150
+
151
+ /** Shared cumulative bar-chart renderer behind renderUsageGraph and renderCostGraph. */
152
+ function renderChart(chart: RenderableChart, width: number, theme: UsageTheme, options: ChartRenderOptions): string[] {
153
+ const { formatValue, unitSuffix } = options;
88
154
  const safeWidth = Math.max(20, width);
89
155
  const chartColumns = Math.max(1, Math.floor((safeWidth - USAGE_Y_AXIS_WIDTH - 1) / 2));
90
156
  const increments = mergeBuckets(chart.buckets, chartColumns);
@@ -97,24 +163,24 @@ export function renderUsageGraph(chart: UsageGraph, width: number, theme: UsageT
97
163
  });
98
164
  const barStep = buckets.length * 2 <= safeWidth - USAGE_Y_AXIS_WIDTH ? 2 : 1;
99
165
  const plotWidth = buckets.length * barStep;
100
- const budget = typeof tokenBudget === "number" && Number.isFinite(tokenBudget) && tokenBudget > 0 ? tokenBudget : undefined;
101
- const maximum = Math.max(chart.totalTokens, budget ?? 0);
102
- const observed = chart.truncated ? `at least ${compact(chart.totalTokens)}` : compact(chart.totalTokens);
166
+ const budget = typeof options.budget === "number" && Number.isFinite(options.budget) && options.budget > 0 ? options.budget : undefined;
167
+ const maximum = Math.max(chart.total, budget ?? 0);
168
+ const observed = chart.truncated ? `at least ${formatValue(chart.total)}` : formatValue(chart.total);
103
169
  const budgetState = budget === undefined
104
- ? `${observed} tokens · budget not configured${chart.truncated ? " · query limit reached" : ""}`
105
- : chart.totalTokens > budget
106
- ? `${observed} / ${compact(budget)} budget · OVER BUDGET by ${chart.truncated ? "at least " : ""}${compact(chart.totalTokens - budget)}`
170
+ ? `${observed}${unitSuffix} · budget not configured${chart.truncated ? " · query limit reached" : ""}`
171
+ : chart.total > budget
172
+ ? `${observed}${unitSuffix} / ${formatValue(budget)} budget · OVER BUDGET by ${chart.truncated ? "at least " : ""}${formatValue(chart.total - budget)}`
107
173
  : chart.truncated
108
- ? `${observed} / ${compact(budget)} budget · state unknown · query limit reached`
109
- : `${observed} / ${compact(budget)} budget · ${compact(budget - chart.totalTokens)} remaining`;
174
+ ? `${observed}${unitSuffix} / ${formatValue(budget)} budget · state unknown · query limit reached`
175
+ : `${observed}${unitSuffix} / ${formatValue(budget)} budget · ${formatValue(budget - chart.total)} remaining`;
110
176
  const lines = [
111
- truncateToWidth(theme.bold(`${usagePeriod(chart.period).label} token usage`), safeWidth, ""),
177
+ truncateToWidth(theme.bold(options.title), safeWidth, ""),
112
178
  truncateToWidth(budgetState, safeWidth, "…"),
113
- truncateToWidth(`input ${compact(chart.breakdown.input)} · output ${compact(chart.breakdown.output)} · cache ${compact(chart.breakdown.cacheRead + chart.breakdown.cacheWrite)}`, safeWidth, "…"),
179
+ ...(options.subtitle ? [truncateToWidth(options.subtitle, safeWidth, "…")] : []),
114
180
  "",
115
181
  ];
116
182
  if (maximum === 0) {
117
- lines.push(theme.fg("dim", "No recorded Pi token usage in this period."));
183
+ lines.push(theme.fg("dim", options.noDataText));
118
184
  return lines.map((line) => truncateToWidth(line, safeWidth, "…"));
119
185
  }
120
186
 
@@ -123,9 +189,9 @@ export function renderUsageGraph(chart: UsageGraph, width: number, theme: UsageT
123
189
  const lower = maximum * fromBottom / USAGE_CHART_HEIGHT;
124
190
  const upper = maximum * (fromBottom + 1) / USAGE_CHART_HEIGHT;
125
191
  const thresholdRow = budget !== undefined && budget > lower && budget <= upper;
126
- const label = thresholdRow ? compact(budget) : row === 0 ? compact(maximum) : row === Math.floor(USAGE_CHART_HEIGHT / 2) ? compact(maximum / 2) : "";
192
+ const label = thresholdRow ? formatValue(budget) : row === 0 ? formatValue(maximum) : row === Math.floor(USAGE_CHART_HEIGHT / 2) ? formatValue(maximum / 2) : "";
127
193
  if (thresholdRow) {
128
- const color = chart.totalTokens > budget ? "error" : "warning";
194
+ const color = chart.total > budget ? "error" : "warning";
129
195
  lines.push(`${label.padStart(USAGE_Y_AXIS_WIDTH - 2)} ${theme.fg("borderMuted", "│")}${theme.fg(color, "┄".repeat(plotWidth))}`);
130
196
  continue;
131
197
  }
@@ -138,24 +204,57 @@ export function renderUsageGraph(chart: UsageGraph, width: number, theme: UsageT
138
204
  continue;
139
205
  }
140
206
  const block = PARTIAL_BLOCKS[Math.max(0, Math.ceil(occupancy * PARTIAL_BLOCKS.length) - 1)]!;
141
- const tokenHeight = Math.min(bucket.total, maximum * (fromBottom + Math.min(occupancy, 0.5)) / USAGE_CHART_HEIGHT);
142
- const color = SERIES_COLORS[seriesAt(bucket, chart, tokenHeight) % SERIES_COLORS.length]!;
143
- plot += theme.fg(color, block) + (barStep === 2 ? " " : "");
207
+ const valueHeight = Math.min(bucket.total, maximum * (fromBottom + Math.min(occupancy, 0.5)) / USAGE_CHART_HEIGHT);
208
+ plot += seriesStyle(seriesAt(bucket, chart, valueHeight), theme)(block) + (barStep === 2 ? " " : "");
144
209
  }
145
210
  lines.push(`${label.padStart(USAGE_Y_AXIS_WIDTH - 2)} ${theme.fg("borderMuted", "│")}${plot}`);
146
211
  }
147
212
  lines.push(`${"0".padStart(USAGE_Y_AXIS_WIDTH - 2)} ${theme.fg("borderMuted", `└${"─".repeat(plotWidth)}`)}`);
148
213
  lines.push(`${" ".repeat(USAGE_Y_AXIS_WIDTH)}${axisLabels(chart.start, chart.end, chart.period, plotWidth)}`);
149
214
  lines.push("");
150
- for (let index = 0; index < chart.series.length; index += 1) {
151
- const series = chart.series[index]!;
152
- const bullet = theme.fg(SERIES_COLORS[index % SERIES_COLORS.length]!, "■");
153
- lines.push(truncateToWidth(`${bullet} ${series.provider}/${series.model} ${compact(series.total)}`, safeWidth, ""));
215
+ const displayedSeries = chart.series.slice(0, USAGE_RENDER_MAX_SERIES);
216
+ for (let index = 0; index < displayedSeries.length; index += 1) {
217
+ const series = displayedSeries[index]!;
218
+ const bullet = seriesStyle(index, theme)("");
219
+ lines.push(truncateToWidth(`${bullet} ${displayIdentity(series.provider)}/${displayIdentity(series.model)} ${formatValue(series.total)}`, safeWidth, "…"));
154
220
  }
221
+ if (chart.series.length > displayedSeries.length) lines.push(truncateToWidth(theme.fg("muted", `… ${chart.series.length - displayedSeries.length} more series omitted`), safeWidth, "…"));
155
222
  return lines.map((line) => visibleWidth(line) <= safeWidth ? line : truncateToWidth(line, safeWidth, "…"));
156
223
  }
157
224
 
158
- async function loadUsage(client: JittorPanelClient, period: UsagePeriod, now: number): Promise<UsageGraph> {
225
+ export function renderUsageGraph(chart: UsageGraph, width: number, theme: UsageTheme, tokenBudget?: number): string[] {
226
+ return renderChart(
227
+ { period: chart.period, start: chart.start, end: chart.end, buckets: chart.buckets, series: chart.series, total: chart.totalTokens, truncated: chart.truncated },
228
+ width, theme,
229
+ {
230
+ title: `${usagePeriod(chart.period).label} token usage`,
231
+ formatValue: compact,
232
+ unitSuffix: " tokens",
233
+ subtitle: `input ${compact(chart.breakdown.input)} · output ${compact(chart.breakdown.output)} · cache ${compact(chart.breakdown.cacheRead + chart.breakdown.cacheWrite)}`,
234
+ budget: tokenBudget,
235
+ noDataText: "No recorded Pi token usage in this period.",
236
+ },
237
+ );
238
+ }
239
+
240
+ export function renderCostGraph(chart: CostGraph, width: number, theme: UsageTheme, costBudget?: number): string[] {
241
+ return renderChart(
242
+ { period: chart.period, start: chart.start, end: chart.end, buckets: chart.buckets, series: chart.series, total: chart.totalUsd, truncated: chart.truncated },
243
+ width, theme,
244
+ {
245
+ title: `${usagePeriod(chart.period).label} cost`,
246
+ formatValue: formatUsd,
247
+ unitSuffix: "",
248
+ budget: costBudget,
249
+ noDataText: "No recorded Pi cost in this period.",
250
+ },
251
+ );
252
+ }
253
+
254
+ export type UsageViewKind = "tokens" | "cost";
255
+ const USAGE_VIEWS: UsageViewKind[] = ["tokens", "cost"];
256
+
257
+ async function loadPiMetrics(client: JittorPanelClient, period: UsagePeriod, now: number): Promise<{ rows: StoredMetricObservation[]; truncated: boolean }> {
159
258
  const rows = await client.call("metrics.query", {
160
259
  source: "pi",
161
260
  since: usagePeriodStart(period, now),
@@ -163,7 +262,7 @@ async function loadUsage(client: JittorPanelClient, period: UsagePeriod, now: nu
163
262
  order: "desc",
164
263
  limit: USAGE_TOKEN_QUERY_LIMIT,
165
264
  }) as StoredMetricObservation[];
166
- return buildUsageGraph(rows, { period, now, truncated: rows.length >= USAGE_TOKEN_QUERY_LIMIT });
265
+ return { rows, truncated: rows.length >= USAGE_TOKEN_QUERY_LIMIT };
167
266
  }
168
267
 
169
268
  export async function showUsagePanel(
@@ -171,32 +270,42 @@ export async function showUsagePanel(
171
270
  client: JittorPanelClient,
172
271
  budgets: Pick<UsageBudgetControl, "getUsageTokenBudget">,
173
272
  now = Date.now(),
273
+ initialView: UsageViewKind = "tokens",
174
274
  ): Promise<void> {
175
275
  let periodIndex = 0;
276
+ let viewIndex = Math.max(0, USAGE_VIEWS.indexOf(initialView));
176
277
  for (;;) {
177
278
  const period = USAGE_PERIODS[periodIndex]!.id;
178
- const chart = await loadUsage(client, period, now);
279
+ // One bounded query serves both views: token and cost metrics share the same "pi" source rows.
280
+ const { rows, truncated } = await loadPiMetrics(client, period, now);
281
+ const view = USAGE_VIEWS[viewIndex]!;
282
+ const tokenChart = buildUsageGraph(rows, { period, now, truncated });
283
+ const costChart = buildCostGraph(rows, { period, now, truncated });
179
284
  const tokenBudget = budgets.getUsageTokenBudget(period);
285
+ const renderActive = (width: number, theme: UsageTheme): string[] =>
286
+ view === "tokens" ? renderUsageGraph(tokenChart, width, theme, tokenBudget) : renderCostGraph(costChart, width, theme);
180
287
  if (ctx.mode !== "tui") {
181
- ctx.ui.notify(renderUsageGraph(chart, 80, plainTheme(), tokenBudget).join("\n"), "info");
288
+ ctx.ui.notify(renderActive(80, plainTheme()).join("\n"), "info");
182
289
  return;
183
290
  }
184
291
  const action = await ctx.ui.custom<UsageAction>((_tui, theme, _keybindings, done) => ({
185
292
  invalidate() {},
186
293
  render(width: number): string[] {
187
- const lines = renderUsageGraph(chart, width, theme, tokenBudget);
188
- const controls = theme.fg("dim", "←/→ period · r refresh · Esc close");
294
+ const lines = renderActive(width, theme);
295
+ const controls = theme.fg("dim", "←/→/Tab period · v view · r refresh · Esc close");
189
296
  return [...lines, "", truncateToWidth(controls, width, "…")];
190
297
  },
191
298
  handleInput(data: string): void {
192
299
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || data === "q") done("close");
193
- else if (matchesKey(data, "left")) done("period-prev");
194
- else if (matchesKey(data, "right")) done("period-next");
300
+ else if (matchesKey(data, "left") || matchesKey(data, "shift+tab")) done("period-prev");
301
+ else if (matchesKey(data, "right") || matchesKey(data, "tab")) done("period-next");
302
+ else if (data === "v") done("view-next");
195
303
  else if (data === "r") done("refresh");
196
304
  },
197
305
  }));
198
306
  if (!action || action === "close") return;
199
307
  if (action === "period-prev") periodIndex = (periodIndex - 1 + USAGE_PERIODS.length) % USAGE_PERIODS.length;
200
308
  if (action === "period-next") periodIndex = (periodIndex + 1) % USAGE_PERIODS.length;
309
+ if (action === "view-next") viewIndex = (viewIndex + 1) % USAGE_VIEWS.length;
201
310
  }
202
311
  }
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@danypops/jittor",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Just-in-Time Token Optimizing Router for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package", "llm-router", "token-budget"],
7
+ "bin": {
8
+ "jittor": "src/cli.ts"
9
+ },
7
10
  "scripts": {
8
11
  "test": "bun test",
9
12
  "typecheck": "tsc --noEmit",
@@ -0,0 +1,99 @@
1
+ import {
2
+ BENCHMARK_MAX_OBSERVATIONS_PER_SNAPSHOT,
3
+ BENCHMARK_STORE_QUERY_LIMIT,
4
+ } from "../constants.ts";
5
+ import {
6
+ validateBenchmarkObservation,
7
+ type BenchmarkObservation,
8
+ type BenchmarkSnapshot,
9
+ } from "../domain/benchmark.ts";
10
+ import type { StoredMetricObservation } from "../domain/metric.ts";
11
+ import type { BenchmarkStore } from "../ports/benchmark-store.ts";
12
+ import type { MetricStore } from "../ports/metric-store.ts";
13
+
14
+ const COMPLETE_METRIC = "snapshot-complete";
15
+ const SNAPSHOT_SCOPE = "snapshot";
16
+
17
+ function metricSource(sourceId: string): string {
18
+ return `benchmark:${sourceId}`;
19
+ }
20
+
21
+ function observationAttributes(snapshotId: string, observation: BenchmarkObservation): Record<string, unknown> {
22
+ return {
23
+ snapshotId,
24
+ model: observation.model,
25
+ provenance: observation.provenance,
26
+ methodology: observation.methodology,
27
+ };
28
+ }
29
+
30
+ function decodeObservation(row: StoredMetricObservation): BenchmarkObservation {
31
+ return validateBenchmarkObservation({
32
+ model: row.attributes["model"],
33
+ dimension: row.metric,
34
+ value: row.value,
35
+ unit: row.unit,
36
+ provenance: row.attributes["provenance"],
37
+ methodology: row.attributes["methodology"],
38
+ });
39
+ }
40
+
41
+ export class MetricBenchmarkStore implements BenchmarkStore {
42
+ constructor(private readonly metrics: MetricStore) {}
43
+
44
+ publish(sourceId: string, snapshotId: string, input: BenchmarkObservation[]): BenchmarkSnapshot {
45
+ if (input.length > BENCHMARK_MAX_OBSERVATIONS_PER_SNAPSHOT) throw new Error("benchmark snapshot exceeds the observation limit");
46
+ const observations = input.map(validateBenchmarkObservation);
47
+ const existing = this.latest(sourceId);
48
+ if (existing?.snapshotId === snapshotId) return existing;
49
+ const retrievedAt = observations[0]?.provenance.retrievedAt;
50
+ if (retrievedAt === undefined || observations.some((observation) => observation.provenance.sourceId !== sourceId || observation.provenance.retrievedAt !== retrievedAt)) {
51
+ throw new Error("benchmark snapshot provenance mismatch");
52
+ }
53
+ for (const observation of observations) {
54
+ this.metrics.record({
55
+ source: metricSource(sourceId),
56
+ scope: observation.model.canonical,
57
+ metric: observation.dimension,
58
+ value: observation.value,
59
+ unit: observation.unit,
60
+ observedAt: retrievedAt,
61
+ attributes: observationAttributes(snapshotId, observation),
62
+ });
63
+ }
64
+ this.metrics.record({
65
+ source: metricSource(sourceId),
66
+ scope: SNAPSHOT_SCOPE,
67
+ metric: COMPLETE_METRIC,
68
+ value: observations.length,
69
+ unit: "count",
70
+ observedAt: retrievedAt,
71
+ attributes: { snapshotId, sourceId, retrievedAt },
72
+ });
73
+ return { sourceId, snapshotId, retrievedAt, publishedAt: retrievedAt, observations: structuredClone(observations) };
74
+ }
75
+
76
+ latest(sourceId: string): BenchmarkSnapshot | null {
77
+ const marker = this.metrics.query({ source: metricSource(sourceId), scope: SNAPSHOT_SCOPE, metric: COMPLETE_METRIC, order: "desc", limit: 1 })[0];
78
+ if (!marker) return null;
79
+ const rows = this.metrics.query({ source: metricSource(sourceId), until: marker.observedAt, order: "desc", limit: BENCHMARK_STORE_QUERY_LIMIT });
80
+ const markerIndex = rows.findIndex((row) => row.id === marker.id);
81
+ if (markerIndex < 0) return null;
82
+ const snapshotId = marker.attributes["snapshotId"];
83
+ const expectedCount = marker.value;
84
+ if (typeof snapshotId !== "string" || typeof expectedCount !== "number" || !Number.isSafeInteger(expectedCount) || expectedCount < 0) return null;
85
+ const matching = rows.slice(markerIndex + 1).filter((row) => row.attributes["snapshotId"] === snapshotId).slice(0, expectedCount);
86
+ if (matching.length !== expectedCount) return null;
87
+ try {
88
+ return {
89
+ sourceId,
90
+ snapshotId,
91
+ retrievedAt: marker.observedAt,
92
+ publishedAt: marker.observedAt,
93
+ observations: matching.reverse().map(decodeObservation),
94
+ };
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,93 @@
1
+ import {
2
+ BENCHMARK_MAX_MODELS_PER_SOURCE,
3
+ BENCHMARK_REFRESH_INTERVAL_MS,
4
+ BENCHMARK_SOURCE_MAX_RESPONSE_BYTES,
5
+ } from "../constants.ts";
6
+ import { normalizeModelIdentity, validateBenchmarkObservation, type BenchmarkObservation, type BenchmarkSourceSnapshot } from "../domain/benchmark.ts";
7
+ import type { BenchmarkSource } from "../ports/benchmark-source.ts";
8
+ import { contractRecord } from "../providers/openrouter-contracts.ts";
9
+ import type { OpenRouterBenchmarkTransport } from "./openrouter-benchmark-source.ts";
10
+
11
+ const SOURCE_ID = "openrouter-artificial-analysis";
12
+ const ENDPOINT = `https://openrouter.ai/api/v1/benchmarks?source=artificial-analysis&task_type=coding&max_results=${BENCHMARK_MAX_MODELS_PER_SOURCE}`;
13
+
14
+ function requiredText(value: unknown, name: string): string {
15
+ if (typeof value !== "string" || value.length === 0 || value.length > 500) throw new Error(`OpenRouter benchmark ${name} schema changed`);
16
+ return value;
17
+ }
18
+
19
+ function requiredNumber(value: unknown, name: string): number {
20
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`OpenRouter benchmark ${name} schema changed`);
21
+ return value;
22
+ }
23
+
24
+ function price(value: unknown, name: string): number {
25
+ const parsed = Number(requiredText(value, name));
26
+ if (!Number.isFinite(parsed) || parsed < 0) throw new Error(`OpenRouter benchmark ${name} schema changed`);
27
+ return parsed;
28
+ }
29
+
30
+ export class OpenRouterBenchmarkIndexSource implements BenchmarkSource {
31
+ readonly id = SOURCE_ID;
32
+
33
+ constructor(
34
+ private readonly apiKey: string,
35
+ private readonly transport: OpenRouterBenchmarkTransport = fetch,
36
+ private readonly clock: () => number = Date.now,
37
+ ) {
38
+ if (apiKey.length === 0) throw new Error("OpenRouter API key is required for benchmark indices");
39
+ }
40
+
41
+ async fetch(): Promise<BenchmarkSourceSnapshot> {
42
+ const response = await this.transport(new Request(ENDPOINT, { headers: { authorization: `Bearer ${this.apiKey}` } }));
43
+ if (!response.ok) throw new Error(`OpenRouter benchmarks failed with HTTP ${response.status}`);
44
+ const text = await response.text();
45
+ if (new TextEncoder().encode(text).byteLength > BENCHMARK_SOURCE_MAX_RESPONSE_BYTES) throw new Error("OpenRouter benchmark response exceeds the size limit");
46
+ let payload: unknown;
47
+ try { payload = JSON.parse(text); } catch { throw new Error("OpenRouter benchmark response is not valid JSON"); }
48
+ const root = contractRecord(payload, "benchmark response");
49
+ const meta = contractRecord(root["meta"], "benchmark metadata");
50
+ if (!Array.isArray(root["data"]) || root["data"].length === 0 || root["data"].length > BENCHMARK_MAX_MODELS_PER_SOURCE) throw new Error("OpenRouter benchmark result count is invalid");
51
+ if (requiredText(meta["source"], "source") !== "artificial-analysis") throw new Error("OpenRouter benchmark source schema changed");
52
+ const version = requiredText(meta["version"], "version");
53
+ const asOf = requiredText(meta["as_of"], "as-of date");
54
+ const publishedAt = Date.parse(asOf);
55
+ if (!Number.isSafeInteger(publishedAt) || publishedAt <= 0) throw new Error("OpenRouter benchmark publication date schema changed");
56
+ const upstreamUrl = new URL(requiredText(meta["source_url"], "source URL"));
57
+ if (upstreamUrl.protocol !== "https:") throw new Error("OpenRouter benchmark source URL must use HTTPS");
58
+ const retrievedAt = this.clock();
59
+ if (!Number.isSafeInteger(retrievedAt) || retrievedAt <= 0) throw new Error("benchmark retrieval time is invalid");
60
+ const revision = `${version}:${asOf}`;
61
+ const observations = root["data"].flatMap((value): BenchmarkObservation[] => {
62
+ const row = contractRecord(value, "benchmark row");
63
+ if (requiredText(row["source"], "row source") !== "artificial-analysis") throw new Error("OpenRouter benchmark row source schema changed");
64
+ const permaslug = requiredText(row["model_permaslug"], "model permaslug");
65
+ const separator = permaslug.indexOf("/");
66
+ if (separator <= 0 || separator === permaslug.length - 1) throw new Error("OpenRouter benchmark model identity schema changed");
67
+ const model = normalizeModelIdentity(permaslug.slice(0, separator), permaslug.slice(separator + 1), [`openrouter/${permaslug}`]);
68
+ const pricing = contractRecord(row["pricing"], "benchmark pricing");
69
+ const provenance = {
70
+ sourceId: SOURCE_ID,
71
+ sourceType: "independent" as const,
72
+ publisher: "Artificial Analysis via OpenRouter",
73
+ url: ENDPOINT,
74
+ revision,
75
+ publishedAt,
76
+ retrievedAt,
77
+ freshUntil: retrievedAt + BENCHMARK_REFRESH_INTERVAL_MS,
78
+ license: "OpenRouter API terms; upstream terms apply",
79
+ confidence: 0.8,
80
+ };
81
+ const common = { model, provenance };
82
+ const methodology = { basis: "Artificial Analysis index via OpenRouter", upstreamUrl: upstreamUrl.toString(), version, asOf };
83
+ return [
84
+ validateBenchmarkObservation({ ...common, dimension: "quality-coding", value: requiredNumber(row["coding_index"], "coding index"), unit: "ratio", methodology }),
85
+ validateBenchmarkObservation({ ...common, dimension: "quality-general", value: requiredNumber(row["intelligence_index"], "intelligence index"), unit: "ratio", methodology }),
86
+ validateBenchmarkObservation({ ...common, dimension: "quality-planning", value: requiredNumber(row["agentic_index"], "agentic index"), unit: "ratio", methodology }),
87
+ validateBenchmarkObservation({ ...common, dimension: "price-input", value: price(pricing["prompt"], "prompt pricing"), unit: "usd", methodology: { ...methodology, basis: "OpenRouter USD per input token" } }),
88
+ validateBenchmarkObservation({ ...common, dimension: "price-output", value: price(pricing["completion"], "completion pricing"), unit: "usd", methodology: { ...methodology, basis: "OpenRouter USD per output token" } }),
89
+ ];
90
+ });
91
+ return { sourceId: this.id, snapshotId: `${this.id}:${revision}`, retrievedAt, observations };
92
+ }
93
+ }
@@ -0,0 +1,109 @@
1
+ import {
2
+ BENCHMARK_MAX_MODELS_PER_SOURCE,
3
+ BENCHMARK_REFRESH_INTERVAL_MS,
4
+ BENCHMARK_SOURCE_MAX_RESPONSE_BYTES,
5
+ BENCHMARK_SOURCE_MAX_TOTAL_RESPONSE_BYTES,
6
+ } from "../constants.ts";
7
+ import { normalizeModelIdentity, validateBenchmarkObservation, type BenchmarkObservation, type BenchmarkSourceSnapshot } from "../domain/benchmark.ts";
8
+ import type { BenchmarkSource } from "../ports/benchmark-source.ts";
9
+ import { contractRecord, parseOpenRouterModels, type OpenRouterModel } from "../providers/openrouter-contracts.ts";
10
+
11
+ const OPENROUTER_MODELS_BASE_URL = "https://openrouter.ai/api/v1/models";
12
+ const OPENROUTER_MODELS_URL = `${OPENROUTER_MODELS_BASE_URL}?limit=${BENCHMARK_MAX_MODELS_PER_SOURCE}`;
13
+ const OPENROUTER_LATENCY_URL = `${OPENROUTER_MODELS_URL}&sort=latency-low-to-high`;
14
+ const OPENROUTER_THROUGHPUT_URL = `${OPENROUTER_MODELS_URL}&sort=throughput-high-to-low`;
15
+ const SOURCE_ID = "openrouter-models";
16
+
17
+ export type OpenRouterBenchmarkTransport = (request: Request) => Promise<Response>;
18
+
19
+ function identity(model: OpenRouterModel) {
20
+ const separator = model.id.indexOf("/");
21
+ if (separator <= 0 || separator === model.id.length - 1) throw new Error("OpenRouter model identity schema changed");
22
+ const provider = model.id.slice(0, separator);
23
+ const modelId = model.id.slice(separator + 1);
24
+ const aliases = [`openrouter/${model.id}`, ...(model.canonicalSlug === model.id ? [] : [model.canonicalSlug])];
25
+ return normalizeModelIdentity(provider, modelId, aliases);
26
+ }
27
+
28
+ function provenance(retrievedAt: number, revision: string, url: string, confidence: number) {
29
+ return {
30
+ sourceId: SOURCE_ID,
31
+ sourceType: "marketplace" as const,
32
+ publisher: "OpenRouter",
33
+ url,
34
+ revision,
35
+ publishedAt: null,
36
+ retrievedAt,
37
+ freshUntil: retrievedAt + BENCHMARK_REFRESH_INTERVAL_MS,
38
+ license: "OpenRouter API terms",
39
+ confidence,
40
+ };
41
+ }
42
+
43
+ function modelObservations(model: OpenRouterModel, retrievedAt: number, revision: string): BenchmarkObservation[] {
44
+ const common = { model: identity(model), provenance: provenance(retrievedAt, revision, OPENROUTER_MODELS_URL, 0.9) };
45
+ return [
46
+ validateBenchmarkObservation({ ...common, dimension: "context-window", value: model.contextLength, unit: "tokens", methodology: { basis: "OpenRouter model context_length" } }),
47
+ ...(model.maxCompletionTokens === null ? [] : [validateBenchmarkObservation({ ...common, dimension: "max-output", value: model.maxCompletionTokens, unit: "tokens", methodology: { basis: "OpenRouter top_provider max_completion_tokens" } })]),
48
+ validateBenchmarkObservation({ ...common, dimension: "price-input", value: model.pricing.prompt, unit: "usd", methodology: { basis: "USD per input token" } }),
49
+ validateBenchmarkObservation({ ...common, dimension: "price-output", value: model.pricing.completion, unit: "usd", methodology: { basis: "USD per output token" } }),
50
+ validateBenchmarkObservation({ ...common, dimension: "parameter-count", value: model.supportedParameters.length, unit: "count", methodology: { basis: "OpenRouter supported_parameters", parameters: [...model.supportedParameters].sort() } }),
51
+ ];
52
+ }
53
+
54
+ function rankObservations(models: OpenRouterModel[], dimension: "latency-rank" | "throughput-rank", retrievedAt: number, revision: string, url: string): BenchmarkObservation[] {
55
+ return models.map((model, index) => validateBenchmarkObservation({
56
+ model: identity(model),
57
+ dimension,
58
+ value: index + 1,
59
+ unit: "count",
60
+ provenance: provenance(retrievedAt, revision, url, 0.7),
61
+ methodology: { basis: dimension === "latency-rank" ? "OpenRouter p50 TTFT server-side ordering" : "OpenRouter p50 throughput server-side ordering", rank: index + 1 },
62
+ }));
63
+ }
64
+
65
+ interface ModelResponse {
66
+ models: OpenRouterModel[];
67
+ etag: string | null;
68
+ bytes: number;
69
+ }
70
+
71
+ export class OpenRouterBenchmarkSource implements BenchmarkSource {
72
+ readonly id = SOURCE_ID;
73
+
74
+ constructor(
75
+ private readonly transport: OpenRouterBenchmarkTransport = fetch,
76
+ private readonly clock: () => number = Date.now,
77
+ ) {}
78
+
79
+ private async readModels(url: string): Promise<ModelResponse> {
80
+ const response = await this.transport(new Request(url));
81
+ if (!response.ok) throw new Error(`OpenRouter models failed with HTTP ${response.status}`);
82
+ const text = await response.text();
83
+ const bytes = new TextEncoder().encode(text).byteLength;
84
+ if (bytes > BENCHMARK_SOURCE_MAX_RESPONSE_BYTES) throw new Error("OpenRouter models response exceeds the size limit");
85
+ let payload: unknown;
86
+ try { payload = JSON.parse(text); } catch { throw new Error("OpenRouter models response is not valid JSON"); }
87
+ const root = contractRecord(payload, "models response");
88
+ if (!Array.isArray(root["data"]) || root["data"].length === 0 || root["data"].length > BENCHMARK_MAX_MODELS_PER_SOURCE) throw new Error("OpenRouter models response exceeds the model limit");
89
+ return { models: parseOpenRouterModels(payload), etag: response.headers.get("etag")?.slice(0, 120) ?? null, bytes };
90
+ }
91
+
92
+ async fetch(): Promise<BenchmarkSourceSnapshot> {
93
+ const [catalog, latency, throughput] = await Promise.all([
94
+ this.readModels(OPENROUTER_MODELS_URL),
95
+ this.readModels(OPENROUTER_LATENCY_URL),
96
+ this.readModels(OPENROUTER_THROUGHPUT_URL),
97
+ ]);
98
+ if (catalog.bytes + latency.bytes + throughput.bytes > BENCHMARK_SOURCE_MAX_TOTAL_RESPONSE_BYTES) throw new Error("OpenRouter model evidence exceeds the total size limit");
99
+ const retrievedAt = this.clock();
100
+ if (!Number.isSafeInteger(retrievedAt) || retrievedAt <= 0) throw new Error("benchmark retrieval time is invalid");
101
+ const revision = catalog.etag || latency.etag || throughput.etag || `retrieved:${retrievedAt}`;
102
+ const observations = [
103
+ ...catalog.models.flatMap((model) => modelObservations(model, retrievedAt, revision)),
104
+ ...rankObservations(latency.models, "latency-rank", retrievedAt, revision, OPENROUTER_LATENCY_URL),
105
+ ...rankObservations(throughput.models, "throughput-rank", retrievedAt, revision, OPENROUTER_THROUGHPUT_URL),
106
+ ];
107
+ return { sourceId: this.id, snapshotId: `${this.id}:${revision}`, retrievedAt, observations };
108
+ }
109
+ }