@danypops/jittor 0.5.1 → 0.7.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 (44) hide show
  1. package/README.md +62 -7
  2. package/docs/BENCHMARK_SOURCES.md +32 -0
  3. package/docs/OUTPUT_CHANNELS.md +31 -0
  4. package/docs/PROVIDER_RESEARCH.md +57 -0
  5. package/extension/src/benchmark-tui.ts +105 -0
  6. package/extension/src/footer.ts +68 -12
  7. package/extension/src/index.ts +263 -36
  8. package/extension/src/tui.ts +61 -9
  9. package/extension/src/usage.ts +165 -47
  10. package/package.json +5 -1
  11. package/src/adapters/metric-benchmark-store.ts +99 -0
  12. package/src/adapters/openrouter-benchmark-index-source.ts +94 -0
  13. package/src/adapters/openrouter-benchmark-source.ts +109 -0
  14. package/src/adapters/sqlite-metric-store.ts +43 -2
  15. package/src/cli.ts +660 -9
  16. package/src/client.ts +12 -44
  17. package/src/constants.ts +69 -5
  18. package/src/daemon.ts +65 -43
  19. package/src/db.ts +13 -30
  20. package/src/domain/benchmark.ts +264 -0
  21. package/src/domain/context-telemetry.ts +31 -0
  22. package/src/domain/metric.ts +46 -5
  23. package/src/domain/model-observation.ts +203 -0
  24. package/src/domain/model-ranking-service.ts +41 -0
  25. package/src/domain/model-ranking.ts +232 -0
  26. package/src/domain/task-cost.ts +70 -0
  27. package/src/domain/task-focus.ts +65 -0
  28. package/src/domain/usage.ts +134 -33
  29. package/src/log.ts +28 -0
  30. package/src/ports/benchmark-controller.ts +11 -0
  31. package/src/ports/benchmark-source.ts +7 -0
  32. package/src/ports/benchmark-store.ts +7 -0
  33. package/src/ports/metric-store.ts +30 -0
  34. package/src/ports/router-controller.ts +1 -0
  35. package/src/providers/anthropic-contracts.ts +127 -0
  36. package/src/providers/google-adc-auth.ts +63 -0
  37. package/src/providers/google-vertex-budget-contracts.ts +181 -0
  38. package/src/providers/google-vertex-budget.ts +127 -0
  39. package/src/providers/google-vertex-contracts.ts +116 -0
  40. package/src/providers/telemetry-sources.ts +35 -0
  41. package/src/router.ts +12 -0
  42. package/src/service.ts +132 -17
  43. package/src/state.ts +31 -57
  44. package/src/version.ts +2 -14
@@ -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";
4
- import type { StoredMetricObservation } from "../../src/domain/metric.ts";
3
+ import { HUMAN_TEXT_FIELD_MAX_CHARACTERS, USAGE_CHART_HEIGHT, USAGE_MAX_DISTINCT_SCOPES, USAGE_RENDER_MAX_SERIES, USAGE_Y_AXIS_WIDTH } from "../../src/constants.ts";
5
4
  import {
5
+ buildCostGraph,
6
6
  buildUsageGraph,
7
+ resolveUsageWindow,
7
8
  USAGE_PERIODS,
8
9
  usagePeriod,
9
- usagePeriodStart,
10
- type UsageBucket,
10
+ type CostGraph,
11
+ type UsageAggregateRow,
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,32 +204,73 @@ 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> {
159
- const rows = await client.call("metrics.query", {
160
- source: "pi",
161
- since: usagePeriodStart(period, now),
162
- until: now,
163
- order: "desc",
164
- limit: USAGE_TOKEN_QUERY_LIMIT,
165
- }) as StoredMetricObservation[];
166
- return buildUsageGraph(rows, { period, now, truncated: rows.length >= USAGE_TOKEN_QUERY_LIMIT });
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
+ /**
258
+ * One bounded round trip: the daemon discovers distinct scopes (still capped at
259
+ * USAGE_MAX_DISTINCT_SCOPES -- more scopes than that is still a real, honestly-reported
260
+ * truncation) and SQL-side aggregates every matching observation into (scope, metric, bucket)
261
+ * sums for the exact window this panel renders. Replaces a per-scope fetch of up to
262
+ * USAGE_PER_SCOPE_QUERY_LIMIT raw rows each, which fixed a *different* problem (one heavy scope
263
+ * starving *other* scopes out of a shared row budget) but could still silently truncate a single
264
+ * heavy scope's *own* older history within the same window -- a real incident: a scope logging
265
+ * tens of thousands of rows a week had its "weekly" chart built from a few minutes of its most
266
+ * recent rows alone. Aggregation has no such failure mode: result size scales with (scopes x
267
+ * metrics x buckets), never with raw event count.
268
+ */
269
+ async function loadPiMetrics(client: JittorPanelClient, window: ReturnType<typeof resolveUsageWindow>): Promise<{ rows: UsageAggregateRow[]; truncated: boolean }> {
270
+ const result = await client.call("metrics.usage_series", {
271
+ source: "pi", since: window.start, until: window.end, bucketSizeMs: window.bucketSizeMs, bucketCount: window.bucketCount, scopeLimit: USAGE_MAX_DISTINCT_SCOPES,
272
+ }) as { rows: UsageAggregateRow[]; truncated: boolean };
273
+ return result;
167
274
  }
168
275
 
169
276
  export async function showUsagePanel(
@@ -171,32 +278,43 @@ export async function showUsagePanel(
171
278
  client: JittorPanelClient,
172
279
  budgets: Pick<UsageBudgetControl, "getUsageTokenBudget">,
173
280
  now = Date.now(),
281
+ initialView: UsageViewKind = "tokens",
174
282
  ): Promise<void> {
175
283
  let periodIndex = 0;
284
+ let viewIndex = Math.max(0, USAGE_VIEWS.indexOf(initialView));
176
285
  for (;;) {
177
286
  const period = USAGE_PERIODS[periodIndex]!.id;
178
- const chart = await loadUsage(client, period, now);
287
+ const window = resolveUsageWindow(period, now);
288
+ // One bounded query serves both views: token and cost metrics share the same "pi" source rows.
289
+ const { rows, truncated } = await loadPiMetrics(client, window);
290
+ const view = USAGE_VIEWS[viewIndex]!;
291
+ const tokenChart = buildUsageGraph(rows, window, { period, truncated });
292
+ const costChart = buildCostGraph(rows, window, { period, truncated });
179
293
  const tokenBudget = budgets.getUsageTokenBudget(period);
294
+ const renderActive = (width: number, theme: UsageTheme): string[] =>
295
+ view === "tokens" ? renderUsageGraph(tokenChart, width, theme, tokenBudget) : renderCostGraph(costChart, width, theme);
180
296
  if (ctx.mode !== "tui") {
181
- ctx.ui.notify(renderUsageGraph(chart, 80, plainTheme(), tokenBudget).join("\n"), "info");
297
+ ctx.ui.notify(renderActive(80, plainTheme()).join("\n"), "info");
182
298
  return;
183
299
  }
184
300
  const action = await ctx.ui.custom<UsageAction>((_tui, theme, _keybindings, done) => ({
185
301
  invalidate() {},
186
302
  render(width: number): string[] {
187
- const lines = renderUsageGraph(chart, width, theme, tokenBudget);
188
- const controls = theme.fg("dim", "←/→ period · r refresh · Esc close");
303
+ const lines = renderActive(width, theme);
304
+ const controls = theme.fg("dim", "←/→/Tab period · v view · r refresh · Esc close");
189
305
  return [...lines, "", truncateToWidth(controls, width, "…")];
190
306
  },
191
307
  handleInput(data: string): void {
192
308
  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");
309
+ else if (matchesKey(data, "left") || matchesKey(data, "shift+tab")) done("period-prev");
310
+ else if (matchesKey(data, "right") || matchesKey(data, "tab")) done("period-next");
311
+ else if (data === "v") done("view-next");
195
312
  else if (data === "r") done("refresh");
196
313
  },
197
314
  }));
198
315
  if (!action || action === "close") return;
199
316
  if (action === "period-prev") periodIndex = (periodIndex - 1 + USAGE_PERIODS.length) % USAGE_PERIODS.length;
200
317
  if (action === "period-next") periodIndex = (periodIndex + 1) % USAGE_PERIODS.length;
318
+ if (action === "view-next") viewIndex = (viewIndex + 1) % USAGE_VIEWS.length;
201
319
  }
202
320
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/jittor",
3
- "version": "0.5.1",
3
+ "version": "0.7.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"],
@@ -22,6 +22,10 @@
22
22
  "@earendil-works/pi-tui": "*",
23
23
  "typebox": "*"
24
24
  },
25
+ "dependencies": {
26
+ "@danypops/daemon-kit": "^0.3.1",
27
+ "google-auth-library": "^10.9.0"
28
+ },
25
29
  "devDependencies": {
26
30
  "bun-types": "latest"
27
31
  },
@@ -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,94 @@
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
+ // agentic_index measures tool-use/agentic execution style, an activity (type), not a subject-matter domain.
87
+ validateBenchmarkObservation({ ...common, dimension: "quality-type-planning", value: requiredNumber(row["agentic_index"], "agentic index"), unit: "ratio", methodology }),
88
+ validateBenchmarkObservation({ ...common, dimension: "price-input", value: price(pricing["prompt"], "prompt pricing"), unit: "usd", methodology: { ...methodology, basis: "OpenRouter USD per input token" } }),
89
+ validateBenchmarkObservation({ ...common, dimension: "price-output", value: price(pricing["completion"], "completion pricing"), unit: "usd", methodology: { ...methodology, basis: "OpenRouter USD per output token" } }),
90
+ ];
91
+ });
92
+ return { sourceId: this.id, snapshotId: `${this.id}:${revision}`, retrievedAt, observations };
93
+ }
94
+ }