@danypops/jittor 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,7 +72,7 @@ The graph fetches metrics per distinct provider/model scope (`jittor metrics dis
72
72
 
73
73
  ### Cost per Papyrus task
74
74
 
75
- Jittor observes Papyrus's task-focus lifecycle in real time over a shared Pi extension event bus (`papyrus.task-focus.v1`) -- Papyrus never depends on Jittor, it only broadcasts which task is currently focused. Every token/cost metric Jittor already records on a finalized Pi assistant message is tagged with the currently focused task's id the moment it is recorded (no time-window estimation, no new instrumentation). A paused or cleared focus stops tagging; spend recorded with nothing focused is reported separately as unattributed, never dropped or folded into an invented task. Run `jittor metrics cost-by-task --since <ms> --until <ms> [--json]` for a bounded per-task breakdown of cost and input/output/cache tokens.
75
+ Jittor observes Papyrus's task-focus lifecycle in real time over a shared Pi extension event bus (`papyrus.task-focus.v1`) -- Papyrus never depends on Jittor, it only broadcasts which task is currently focused. Every token/cost metric Jittor already records on a finalized Pi assistant message is tagged with the currently focused task's id, and the provider/model/thinking level active at that moment, the instant it is recorded (no time-window estimation, no new instrumentation). A paused or cleared focus stops tagging; spend recorded with nothing focused is reported separately as unattributed, never dropped or folded into an invented task. Run `jittor metrics cost-by-task --since <ms> --until <ms> [--json]` for a bounded per-task breakdown of cost and input/output/cache tokens, broken down further by which provider/model/thinking combination each task actually spent on.
76
76
 
77
77
  Series are colored with a categorical palette chosen to avoid this UI's own status colors ("success"/"warning"/"error" already mean something specific elsewhere in this panel, so reusing them for arbitrary model identity would make a model's bar segment look like a warning or a failure) and instead reuses the theme's syntax-highlighting roles, which are already tuned by theme authors to stay mutually distinguishable on screen — the same design problem as a categorical data palette. Once more series are active than there are hues, a series reuses a hue in bold rather than repeating an indistinguishable color. Multiple models active within the same cumulative time frame are rendered as one bar stacked by color, not separate bars.
78
78
 
@@ -80,9 +80,11 @@ Token-budget thresholds are optional and must be configured by the user; Jittor
80
80
 
81
81
  ### Benchmark evidence
82
82
 
83
- Jittor can ingest bounded OpenRouter model metadata, p50 latency/throughput ordering, versioned Artificial Analysis benchmark indices, and Design Arena Elo rankings as provenance-bearing evidence without treating OpenRouter as model-scope authority. Enable online ingestion explicitly with `JITTOR_OPENROUTER_BENCHMARKS=1`; it is off by default. OpenRouter model metadata and operational ordering are public; benchmark-index and Design Arena ingestion additionally use `OPENROUTER_API_KEY` from the supervised service environment without retaining it. Snapshots preserve the upstream publisher, normalized model identities, immutable retrieval revisions, source URLs, confidence, license terms, and explicit freshness deadlines. A malformed or oversized refresh leaves the last complete snapshot visible and records only a payload-safe failure state.
83
+ Jittor can ingest bounded OpenRouter model metadata, p50 latency/throughput ordering, and Design Arena Elo rankings as provenance-bearing evidence without treating OpenRouter as model-scope authority. Enable online ingestion explicitly with `JITTOR_OPENROUTER_BENCHMARKS=1`; it is off by default. OpenRouter model metadata and operational ordering are public; Design Arena ingestion additionally uses `OPENROUTER_API_KEY` from the supervised service environment without retaining it. Snapshots preserve the upstream publisher, normalized model identities, immutable retrieval revisions, source URLs, confidence, license terms, and explicit freshness deadlines. A malformed or oversized refresh leaves the last complete snapshot visible and records only a payload-safe failure state.
84
84
 
85
- Design Arena rates models across dozens of arena/category pairs (music, video, text-to-speech, ASCII art, ...); Jittor ingests only the bounded allowlist of categories (`codecategories`, `website`, `uicomponent`, `dataviz`, `svg`) that measure frontend/UI-generation skill relevant to routing a coding agent, tagged into one `design` domain distinct from `coding`. A model with no OpenRouter-reachable identity (proprietary platforms, image/video generators) is skipped rather than fabricated into unroutable evidence.
85
+ Design Arena rates models across dozens of arena/category pairs (music, video, text-to-speech, ASCII art, ...); Jittor ingests only the bounded allowlist of categories (`codecategories`, `website`, `uicomponent`, `dataviz`, `svg`) that measure frontend/UI-generation skill relevant to routing a coding agent, tagged into one `design` domain distinct from `coding`. A model with no OpenRouter-reachable identity (proprietary platforms, image/video generators) is skipped rather than fabricated into unroutable evidence. Kept on the OpenRouter passthrough rather than migrated to a direct integration: Design Arena's own native API requires a manually reviewed application (1-2 business days), unlike Artificial Analysis's instant self-serve signup below.
86
+
87
+ Jittor also ingests LMArena's own official Hugging Face dataset (`lmarena-ai/leaderboard-dataset`, via the public `datasets-server.huggingface.co` API -- no credential required) for its Code Arena (`webdev`) and Agent Arena human-preference battles, and, when `ARTIFICIAL_ANALYSIS_API_KEY` is configured, Artificial Analysis's own direct API (replaces the former OpenRouter passthrough to the same publisher; adds a `math` domain and measured per-model latency the passthrough never exposed). LMArena's Bradley-Terry/IPS ratings aren't on the same scale as Artificial Analysis's 0-100 indices, so they're tagged under distinct `-arena`-suffixed dimensions (`quality-coding-arena`, `quality-type-planning-arena`) instead of blended into the same average -- stored and queryable on their own, not yet part of the default ranked "quality" score.
86
88
 
87
89
  Use the authenticated CLI channels independently:
88
90
 
@@ -85,7 +85,7 @@ export async function showBenchmarkPanel(
85
85
  context: MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
86
86
  reliability: MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
87
87
  },
88
- sourceIds: ["openrouter-models", "openrouter-artificial-analysis", "openrouter-design-arena"],
88
+ sourceIds: ["openrouter-models", "lmarena-hf", "artificial-analysis-direct", "openrouter-design-arena"],
89
89
  }) as ModelRankingResult;
90
90
  if (ctx.mode !== "tui") {
91
91
  ctx.ui.notify(renderBenchmarkView(result, currentIdentity, 100, { fg: (_color, text) => text, bold: (text) => text }).join("\n"), "info");
@@ -230,8 +230,12 @@ async function applyDecision(
230
230
  return halt(ctx, `Jittor could not apply any authenticated Pi route after ${decision.route.provider}/${decision.route.model} became unavailable`);
231
231
  }
232
232
 
233
- /** taskId, when a Papyrus task is currently focused in this session, tags the metric for real-time cost-per-task correlation without any new instrumentation surface. */
234
- function assistantUsageMetrics(message: unknown, observedAt: number, taskId: string | null = null): MetricObservation[] {
233
+ /**
234
+ * taskId, when a Papyrus task is focused, tags the metric for cost-per-task correlation. thinking
235
+ * comes from pi.getThinkingLevel() at message_end time, not from the message itself -- AssistantMessage
236
+ * has no thinking field of its own, and the level can't have changed mid-message.
237
+ */
238
+ function assistantUsageMetrics(message: unknown, observedAt: number, taskId: string | null = null, thinking: string | null = null): MetricObservation[] {
235
239
  if (typeof message !== "object" || message === null || Array.isArray(message)) return [];
236
240
  const value = message as Record<string, unknown>;
237
241
  if (value["role"] !== "assistant" || typeof value["usage"] !== "object" || value["usage"] === null) return [];
@@ -239,7 +243,7 @@ function assistantUsageMetrics(message: unknown, observedAt: number, taskId: str
239
243
  const provider = typeof value["provider"] === "string" ? value["provider"] : "unknown";
240
244
  const model = typeof value["model"] === "string" ? value["model"] : "unknown";
241
245
  const scope = `${provider}:${model}`;
242
- const attributes = { provider, model, ...(taskId === null ? {} : { taskId }) };
246
+ const attributes = { provider, model, ...(taskId === null ? {} : { taskId }), ...(thinking === null || thinking.length === 0 ? {} : { thinking }) };
243
247
  const metrics: MetricObservation[] = [];
244
248
  for (const [field, metric] of [["input", "input-tokens"], ["output", "output-tokens"], ["cacheRead", "cache-read-tokens"], ["cacheWrite", "cache-write-tokens"]] as const) {
245
249
  const amount = usage[field];
@@ -796,7 +800,7 @@ export function registerJittorExtension(
796
800
  }
797
801
  lastAnthropicVertexResponse = {};
798
802
  }
799
- const metrics = assistantUsageMetrics(event.message, Date.now(), focusedTaskId);
803
+ const metrics = assistantUsageMetrics(event.message, Date.now(), focusedTaskId, pi.getThinkingLevel());
800
804
  if (metrics.length > 0) {
801
805
  const amount = (name: string): number => metrics.filter((metric) => metric.metric === name && typeof metric.value === "number").reduce((sum, metric) => sum + (metric.value ?? 0), 0);
802
806
  compactionTelemetry.observeProviderUsage({ input: amount("input-tokens"), output: amount("output-tokens"), cacheRead: amount("cache-read-tokens"), cacheWrite: amount("cache-write-tokens") });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/jittor",
3
- "version": "0.8.1",
3
+ "version": "0.10.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"],
@@ -0,0 +1,80 @@
1
+ import { BENCHMARK_MAX_MODELS_PER_SOURCE, BENCHMARK_REFRESH_INTERVAL_MS, BENCHMARK_SOURCE_MAX_RESPONSE_BYTES } from "../constants.ts";
2
+ import { normalizeModelIdentity, validateBenchmarkObservation, type BenchmarkObservation, type BenchmarkSourceSnapshot } from "../domain/benchmark.ts";
3
+ import type { BenchmarkSource } from "../ports/benchmark-source.ts";
4
+ import { contractRecord } from "../providers/openrouter-contracts.ts";
5
+
6
+ const SOURCE_ID = "artificial-analysis-direct";
7
+ const ENDPOINT = "https://artificialanalysis.ai/api/v2/data/llms/models";
8
+
9
+ export type ArtificialAnalysisTransport = (request: Request) => Promise<Response>;
10
+
11
+ function requiredText(value: unknown, name: string): string {
12
+ if (typeof value !== "string" || value.length === 0 || value.length > 500) throw new Error(`Artificial Analysis ${name} schema changed`);
13
+ return value;
14
+ }
15
+
16
+ function requiredNumber(value: unknown, name: string): number {
17
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Artificial Analysis ${name} schema changed`);
18
+ return value;
19
+ }
20
+
21
+ /** Replaces the removed OpenRouter passthrough to the same publisher; adds math_index and measured latency it never exposed. Same dimension names, since it's the same facts. */
22
+ export class ArtificialAnalysisDirectSource implements BenchmarkSource {
23
+ readonly id = SOURCE_ID;
24
+
25
+ constructor(
26
+ private readonly apiKey: string,
27
+ private readonly transport: ArtificialAnalysisTransport = fetch,
28
+ private readonly clock: () => number = Date.now,
29
+ ) {
30
+ if (apiKey.length === 0) throw new Error("Artificial Analysis API key is required");
31
+ }
32
+
33
+ async fetch(): Promise<BenchmarkSourceSnapshot> {
34
+ const retrievedAt = this.clock();
35
+ if (!Number.isSafeInteger(retrievedAt) || retrievedAt <= 0) throw new Error("benchmark retrieval time is invalid");
36
+ const response = await this.transport(new Request(ENDPOINT, { headers: { "x-api-key": this.apiKey } }));
37
+ if (!response.ok) throw new Error(`Artificial Analysis benchmarks failed with HTTP ${response.status}`);
38
+ const text = await response.text();
39
+ if (new TextEncoder().encode(text).byteLength > BENCHMARK_SOURCE_MAX_RESPONSE_BYTES) throw new Error("Artificial Analysis benchmark response exceeds the size limit");
40
+ let payload: unknown;
41
+ try { payload = JSON.parse(text); } catch { throw new Error("Artificial Analysis benchmark response is not valid JSON"); }
42
+ const root = contractRecord(payload, "benchmark response");
43
+ if (!Array.isArray(root["data"]) || root["data"].length > BENCHMARK_MAX_MODELS_PER_SOURCE) throw new Error("Artificial Analysis benchmark result count is invalid");
44
+ const revision = String(retrievedAt);
45
+ const observations = root["data"].flatMap((value): BenchmarkObservation[] => {
46
+ const row = contractRecord(value, "benchmark row");
47
+ const creator = contractRecord(row["model_creator"], "model creator");
48
+ const evaluations = contractRecord(row["evaluations"], "evaluations");
49
+ const identity = normalizeModelIdentity(requiredText(creator["slug"], "creator slug"), requiredText(row["slug"], "model slug"), [`artificial-analysis/${requiredText(row["id"], "model id")}`]);
50
+ const provenance = {
51
+ sourceId: SOURCE_ID,
52
+ sourceType: "creator" as const,
53
+ publisher: "Artificial Analysis",
54
+ url: "https://artificialanalysis.ai/",
55
+ revision,
56
+ publishedAt: retrievedAt,
57
+ retrievedAt,
58
+ freshUntil: retrievedAt + BENCHMARK_REFRESH_INTERVAL_MS,
59
+ license: "Attribution to artificialanalysis.ai required; see their free API terms",
60
+ confidence: 0.85,
61
+ };
62
+ const methodology = { basis: "Artificial Analysis direct API, /data/llms/models" };
63
+ const observations: BenchmarkObservation[] = [];
64
+ const index = (field: string, dimension: string): void => {
65
+ const raw = evaluations[field];
66
+ if (raw === undefined || raw === null) return;
67
+ observations.push(validateBenchmarkObservation({ model: identity, dimension, value: requiredNumber(raw, field), unit: "ratio", provenance, methodology }));
68
+ };
69
+ index("artificial_analysis_coding_index", "quality-coding");
70
+ index("artificial_analysis_intelligence_index", "quality-general");
71
+ index("artificial_analysis_math_index", "quality-math");
72
+ const ttft = row["median_time_to_first_token_seconds"];
73
+ if (typeof ttft === "number" && Number.isFinite(ttft)) {
74
+ observations.push(validateBenchmarkObservation({ model: identity, dimension: "latency", value: ttft * 1_000, unit: "milliseconds", provenance, methodology }));
75
+ }
76
+ return observations;
77
+ });
78
+ return { sourceId: this.id, snapshotId: `${this.id}:${revision}`, retrievedAt, observations };
79
+ }
80
+ }
@@ -0,0 +1,111 @@
1
+ import { BENCHMARK_MAX_MODELS_PER_SOURCE, BENCHMARK_REFRESH_INTERVAL_MS, BENCHMARK_SOURCE_MAX_RESPONSE_BYTES } from "../constants.ts";
2
+ import { normalizeModelIdentity, validateBenchmarkObservation, type BenchmarkObservation, type BenchmarkSourceSnapshot } from "../domain/benchmark.ts";
3
+ import type { BenchmarkSource } from "../ports/benchmark-source.ts";
4
+ import { contractRecord } from "../providers/openrouter-contracts.ts";
5
+
6
+ const SOURCE_ID = "lmarena-hf";
7
+ const BASE_URL = "https://datasets-server.huggingface.co/rows";
8
+ /** The server's own per-page cap; observed directly against the live API. */
9
+ const HF_ROWS_PER_PAGE = 100;
10
+
11
+ export type LmArenaTransport = (request: Request) => Promise<Response>;
12
+
13
+ /**
14
+ * Limited to webdev (Code Arena) and agent (Agent Arena); text/text_style_control's "coding"
15
+ * category exists but its "latest" split spans tens of thousands of rows, unbounded compared to
16
+ * these two's few hundred.
17
+ *
18
+ * Dimensions are distinct from AA's (quality-coding-arena, not quality-coding) because the
19
+ * scales don't match: Bradley-Terry rating (~1200-1700) and IPS score (~0) vs AA's 0-100 ratio.
20
+ * withEvidence() averages same-named dimensions with no unit conversion, so sharing a name would
21
+ * blend incompatible scales into a meaningless number. Stored and queryable on their own,
22
+ * intentionally not folded into models.rank's blended "quality" component yet.
23
+ */
24
+ const ARENAS: ReadonlyArray<{ config: string; dimension: string; unit: "ratio" }> = [
25
+ { config: "webdev", dimension: "quality-coding-arena", unit: "ratio" },
26
+ { config: "agent", dimension: "quality-type-planning-arena", unit: "ratio" },
27
+ ];
28
+
29
+ const THINKING_SUFFIX = /\s*\((?:high|xhigh|low|medium|max|fast|thinking|codex-harness)\)\s*$/i;
30
+
31
+ function requiredText(value: unknown, name: string): string {
32
+ if (typeof value !== "string" || value.length === 0 || value.length > 500) throw new Error(`LMArena benchmark ${name} schema changed`);
33
+ return value;
34
+ }
35
+
36
+ function requiredNumber(value: unknown, name: string): number {
37
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`LMArena benchmark ${name} schema changed`);
38
+ return value;
39
+ }
40
+
41
+ /**
42
+ * Best-effort: LMArena has no machine identity field, just a display name and org slug. A wrong
43
+ * guess is safe, not a correctness bug -- identity only ever matches exactly against real
44
+ * candidates the caller already supplied, so a bad slug or an unreleased codename ("Inkling")
45
+ * is just inert evidence, never misattributed evidence.
46
+ */
47
+ function bestEffortIdentity(organization: string, displayName: string): { provider: string; model: string; aliases: string[] } {
48
+ const base = displayName.replace(THINKING_SUFFIX, "").trim();
49
+ const slug = base.toLowerCase().replace(/[^a-z0-9.]+/g, "-").replace(/^-+|-+$/g, "");
50
+ return { provider: organization, model: slug, aliases: [displayName.toLowerCase()] };
51
+ }
52
+
53
+ export class LmArenaHfSource implements BenchmarkSource {
54
+ readonly id = SOURCE_ID;
55
+
56
+ constructor(
57
+ private readonly transport: LmArenaTransport = fetch,
58
+ private readonly clock: () => number = Date.now,
59
+ ) {}
60
+
61
+ async fetch(): Promise<BenchmarkSourceSnapshot> {
62
+ const retrievedAt = this.clock();
63
+ if (!Number.isSafeInteger(retrievedAt) || retrievedAt <= 0) throw new Error("benchmark retrieval time is invalid");
64
+ const perArena = await Promise.all(ARENAS.map((arena) => this.fetchArena(arena, retrievedAt)));
65
+ const observations = perArena.flatMap((page) => page.observations).slice(0, BENCHMARK_MAX_MODELS_PER_SOURCE * ARENAS.length);
66
+ const asOf = perArena.map((page) => page.asOf).sort().at(-1) ?? "unknown";
67
+ return { sourceId: this.id, snapshotId: `${this.id}:${asOf}`, retrievedAt, observations };
68
+ }
69
+
70
+ private async fetchArena(arena: { config: string; dimension: string; unit: "ratio" }, retrievedAt: number): Promise<{ asOf: string; observations: BenchmarkObservation[] }> {
71
+ const url = `${BASE_URL}?dataset=lmarena-ai%2Fleaderboard-dataset&config=${arena.config}&split=latest&length=${HF_ROWS_PER_PAGE}`;
72
+ const response = await this.transport(new Request(url));
73
+ if (!response.ok) throw new Error(`LMArena benchmark fetch failed with HTTP ${response.status}`);
74
+ const text = await response.text();
75
+ if (new TextEncoder().encode(text).byteLength > BENCHMARK_SOURCE_MAX_RESPONSE_BYTES) throw new Error("LMArena benchmark response exceeds the size limit");
76
+ let payload: unknown;
77
+ try { payload = JSON.parse(text); } catch { throw new Error("LMArena benchmark response is not valid JSON"); }
78
+ const root = contractRecord(payload, "benchmark response");
79
+ if (!Array.isArray(root["rows"]) || root["rows"].length > HF_ROWS_PER_PAGE) throw new Error("LMArena benchmark row count is invalid");
80
+ let asOf = "";
81
+ const observations = root["rows"].flatMap((entry): BenchmarkObservation[] => {
82
+ const wrapper = contractRecord(entry, "benchmark row wrapper");
83
+ const row = contractRecord(wrapper["row"], "benchmark row");
84
+ const publishDate = requiredText(row["leaderboard_publish_date"], "publish date");
85
+ if (publishDate > asOf) asOf = publishDate;
86
+ // Skip finer per-category splits, if any exist for this config -- out of scope for now.
87
+ if (requiredText(row["category"], "category") !== "overall") return [];
88
+ const scoreValue = row["rating"] ?? row["score"];
89
+ const value = requiredNumber(scoreValue, "score");
90
+ const organization = requiredText(row["organization"], "organization");
91
+ const displayName = requiredText(row["model_name"], "model name");
92
+ const guessed = bestEffortIdentity(organization, displayName);
93
+ const identity = normalizeModelIdentity(guessed.provider, guessed.model, guessed.aliases);
94
+ const provenance = {
95
+ sourceId: SOURCE_ID,
96
+ sourceType: "preference" as const,
97
+ publisher: "LMArena",
98
+ url: "https://huggingface.co/datasets/lmarena-ai/leaderboard-dataset",
99
+ revision: `${arena.config}:${publishDate}`,
100
+ publishedAt: Date.parse(publishDate) || null,
101
+ retrievedAt,
102
+ freshUntil: retrievedAt + BENCHMARK_REFRESH_INTERVAL_MS,
103
+ license: "See lmarena-ai/leaderboard-dataset on Hugging Face for per-model license terms",
104
+ confidence: 0.6,
105
+ };
106
+ const methodology = { basis: "LMArena human-preference battles", arena: arena.config, displayName, publishDate };
107
+ return [validateBenchmarkObservation({ model: identity, dimension: arena.dimension, value, unit: arena.unit, provenance, methodology })];
108
+ });
109
+ return { asOf, observations };
110
+ }
111
+ }
package/src/cli.ts CHANGED
@@ -573,7 +573,10 @@ function formatUsdAmount(amount: number): string {
573
573
  export function formatCostByTask(summary: TaskCostSummary): string {
574
574
  const lines = [
575
575
  `Cost by task: ${summary.entries.length.toLocaleString()} task(s)${summary.truncated ? " (query limit reached; totals are a lower bound)" : ""}`,
576
- ...summary.entries.map((entry) => `- ${humanField(entry.taskId)}: ${formatUsdAmount(entry.costUsd)} · ↑${entry.inputTokens.toLocaleString()} ↓${entry.outputTokens.toLocaleString()} R${entry.cacheReadTokens.toLocaleString()} W${entry.cacheWriteTokens.toLocaleString()}`),
576
+ ...summary.entries.flatMap((entry) => [
577
+ `- ${humanField(entry.taskId)}: ${formatUsdAmount(entry.costUsd)} · ↑${entry.inputTokens.toLocaleString()} ↓${entry.outputTokens.toLocaleString()} R${entry.cacheReadTokens.toLocaleString()} W${entry.cacheWriteTokens.toLocaleString()}`,
578
+ ...entry.byModel.map((model) => ` · ${humanField(model.provider)}/${humanField(model.model)} (${humanField(model.thinking)}): ${formatUsdAmount(model.costUsd)} · ↑${model.inputTokens.toLocaleString()} ↓${model.outputTokens.toLocaleString()} R${model.cacheReadTokens.toLocaleString()} W${model.cacheWriteTokens.toLocaleString()}`),
579
+ ]),
577
580
  `Unattributed spend (no task was focused): ${formatUsdAmount(summary.unattributedCostUsd)}`,
578
581
  ];
579
582
  return lines.join("\n");
package/src/daemon.ts CHANGED
@@ -3,9 +3,10 @@ import { MAINTENANCE_INTERVAL_MS, TELEMETRY_POLL_INTERVAL_MS } from "./constants
3
3
  import { DEFAULT_POLICY, UNCONFIGURED_ROUTE } from "./config.ts";
4
4
  import { SQLiteMetricStore } from "./adapters/sqlite-metric-store.ts";
5
5
  import { MetricBenchmarkStore } from "./adapters/metric-benchmark-store.ts";
6
- import { OpenRouterBenchmarkIndexSource } from "./adapters/openrouter-benchmark-index-source.ts";
7
6
  import { OpenRouterBenchmarkSource } from "./adapters/openrouter-benchmark-source.ts";
8
7
  import { OpenRouterDesignArenaSource } from "./adapters/openrouter-design-arena-source.ts";
8
+ import { LmArenaHfSource } from "./adapters/lmarena-hf-source.ts";
9
+ import { ArtificialAnalysisDirectSource } from "./adapters/artificial-analysis-direct-source.ts";
9
10
  import { openJittorDb } from "./db.ts";
10
11
  import { BenchmarkCatalog } from "./domain/benchmark.ts";
11
12
  import { EvidenceModelRanker } from "./domain/model-ranking-service.ts";
@@ -26,13 +27,15 @@ export function reportMaintenanceFailure(event: string, error: unknown): void {
26
27
  logEvent("error", event, { message: error instanceof Error ? error.message : String(error) });
27
28
  }
28
29
 
30
+ // Flag name predates non-OpenRouter sources; kept as the one "opt into online benchmark
31
+ // ingestion" toggle rather than adding a second flag for the same decision.
29
32
  export function benchmarkSourcesFromEnvironment(env: Record<string, string | undefined> = process.env): BenchmarkSource[] {
30
33
  if (env["JITTOR_OPENROUTER_BENCHMARKS"] !== "1") return [];
31
- const sources: BenchmarkSource[] = [new OpenRouterBenchmarkSource()];
32
- if (env["OPENROUTER_API_KEY"]) {
33
- sources.push(new OpenRouterBenchmarkIndexSource(env["OPENROUTER_API_KEY"]));
34
- sources.push(new OpenRouterDesignArenaSource(env["OPENROUTER_API_KEY"]));
35
- }
34
+ const sources: BenchmarkSource[] = [new OpenRouterBenchmarkSource(), new LmArenaHfSource()];
35
+ // Design Arena's own API needs manual approval, unlike Artificial Analysis's instant signup --
36
+ // no direct alternative exists yet, so the OpenRouter passthrough stays.
37
+ if (env["OPENROUTER_API_KEY"]) sources.push(new OpenRouterDesignArenaSource(env["OPENROUTER_API_KEY"]));
38
+ if (env["ARTIFICIAL_ANALYSIS_API_KEY"]) sources.push(new ArtificialAnalysisDirectSource(env["ARTIFICIAL_ANALYSIS_API_KEY"]));
36
39
  return sources;
37
40
  }
38
41
 
@@ -16,7 +16,7 @@ import type { MetricObservation, MetricUnit, StoredMetricObservation } from "./m
16
16
  * usage carries no distinguishing signal for that axis; a run can score coding on domain and
17
17
  * research on type simultaneously (e.g. reading a file, then searching the web in one turn).
18
18
  */
19
- export const TASK_DOMAINS = ["coding", "design", "general"] as const;
19
+ export const TASK_DOMAINS = ["coding", "design", "math", "general"] as const;
20
20
  export type ModelTaskDomain = typeof TASK_DOMAINS[number];
21
21
  export const TASK_TYPES = ["research", "planning", "general"] as const;
22
22
  export type ModelTaskType = typeof TASK_TYPES[number];
@@ -1,5 +1,16 @@
1
1
  import type { StoredMetricObservation } from "./metric.ts";
2
2
 
3
+ export interface TaskCostBreakdown {
4
+ provider: string;
5
+ model: string;
6
+ thinking: string;
7
+ costUsd: number;
8
+ inputTokens: number;
9
+ outputTokens: number;
10
+ cacheReadTokens: number;
11
+ cacheWriteTokens: number;
12
+ }
13
+
3
14
  export interface TaskCostEntry {
4
15
  taskId: string;
5
16
  costUsd: number;
@@ -7,6 +18,7 @@ export interface TaskCostEntry {
7
18
  outputTokens: number;
8
19
  cacheReadTokens: number;
9
20
  cacheWriteTokens: number;
21
+ byModel: TaskCostBreakdown[];
10
22
  }
11
23
 
12
24
  export interface TaskCostSummary {
@@ -25,20 +37,35 @@ export interface TaskCostSummaryOptions {
25
37
 
26
38
  const TOKEN_METRICS = new Set(["input-tokens", "output-tokens", "cache-read-tokens", "cache-write-tokens"]);
27
39
 
40
+ function attributeText(attributes: Record<string, unknown>, key: string): string {
41
+ return typeof attributes[key] === "string" && attributes[key].length > 0 ? attributes[key] : "unknown";
42
+ }
43
+
28
44
  function entryFor(byTask: Map<string, TaskCostEntry>, taskId: string): TaskCostEntry {
29
45
  const existing = byTask.get(taskId);
30
46
  if (existing) return existing;
31
- const created: TaskCostEntry = { taskId, costUsd: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
47
+ const created: TaskCostEntry = { taskId, costUsd: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, byModel: [] };
32
48
  byTask.set(taskId, created);
33
49
  return created;
34
50
  }
35
51
 
52
+ function breakdownFor(byModel: Map<string, TaskCostBreakdown>, provider: string, model: string, thinking: string): TaskCostBreakdown {
53
+ const key = `${provider}\u0000${model}\u0000${thinking}`;
54
+ const existing = byModel.get(key);
55
+ if (existing) return existing;
56
+ const created: TaskCostBreakdown = { provider, model, thinking, costUsd: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
57
+ byModel.set(key, created);
58
+ return created;
59
+ }
60
+
36
61
  /**
37
62
  * Groups already-recorded "pi" source cost/token metrics by the Papyrus task focused when each was
38
- * recorded (see the papyrus.task-focus.v1 real-time tagging in the extension), rather than by
39
- * provider/model identity. Rows recorded with nothing focused have no attributes.taskId and are
40
- * reported separately as unattributedCostUsd -- they are real spend, just not attributable to any
41
- * task, and must never be silently dropped or folded into an invented "unknown" task bucket.
63
+ * recorded (see the papyrus.task-focus.v1 real-time tagging in the extension), with a secondary
64
+ * breakdown per provider/model/thinking within each task. Rows recorded with nothing focused have
65
+ * no attributes.taskId and are reported separately as unattributedCostUsd -- they are real spend,
66
+ * just not attributable to any task, and must never be silently dropped or folded into an invented
67
+ * "unknown" task bucket. A row missing provider/model/thinking (recorded before that attribution
68
+ * existed) falls into an "unknown" breakdown bucket instead, since the task-level total is real.
42
69
  *
43
70
  * This queries a single bounded time window without per-task fairness partitioning (unlike
44
71
  * buildUsageGraph/buildCostGraph's per-scope fetch): a task's own working period is typically a far
@@ -48,6 +75,7 @@ function entryFor(byTask: Map<string, TaskCostEntry>, taskId: string): TaskCostE
48
75
  */
49
76
  export function buildTaskCostSummary(rows: StoredMetricObservation[], options: TaskCostSummaryOptions): TaskCostSummary {
50
77
  const byTask = new Map<string, TaskCostEntry>();
78
+ const byTaskModel = new Map<string, Map<string, TaskCostBreakdown>>();
51
79
  let unattributedCostUsd = 0;
52
80
  for (const row of rows) {
53
81
  if (row.source !== "pi" || typeof row.value !== "number" || !Number.isFinite(row.value) || row.value < 0) continue;
@@ -56,14 +84,21 @@ export function buildTaskCostSummary(rows: StoredMetricObservation[], options: T
56
84
  if (row.metric === "cost" && row.unit === "usd") {
57
85
  if (taskId === undefined) { unattributedCostUsd += row.value; continue; }
58
86
  entryFor(byTask, taskId).costUsd += row.value;
87
+ if (!byTaskModel.has(taskId)) byTaskModel.set(taskId, new Map());
88
+ breakdownFor(byTaskModel.get(taskId)!, attributeText(row.attributes, "provider"), attributeText(row.attributes, "model"), attributeText(row.attributes, "thinking")).costUsd += row.value;
59
89
  continue;
60
90
  }
61
91
  if (taskId === undefined || row.unit !== "tokens" || !TOKEN_METRICS.has(row.metric)) continue;
62
92
  const entry = entryFor(byTask, taskId);
63
- if (row.metric === "input-tokens") entry.inputTokens += row.value;
64
- else if (row.metric === "output-tokens") entry.outputTokens += row.value;
65
- else if (row.metric === "cache-read-tokens") entry.cacheReadTokens += row.value;
66
- else entry.cacheWriteTokens += row.value;
93
+ if (!byTaskModel.has(taskId)) byTaskModel.set(taskId, new Map());
94
+ const breakdown = breakdownFor(byTaskModel.get(taskId)!, attributeText(row.attributes, "provider"), attributeText(row.attributes, "model"), attributeText(row.attributes, "thinking"));
95
+ if (row.metric === "input-tokens") { entry.inputTokens += row.value; breakdown.inputTokens += row.value; }
96
+ else if (row.metric === "output-tokens") { entry.outputTokens += row.value; breakdown.outputTokens += row.value; }
97
+ else if (row.metric === "cache-read-tokens") { entry.cacheReadTokens += row.value; breakdown.cacheReadTokens += row.value; }
98
+ else { entry.cacheWriteTokens += row.value; breakdown.cacheWriteTokens += row.value; }
99
+ }
100
+ for (const [taskId, entry] of byTask) {
101
+ entry.byModel = [...(byTaskModel.get(taskId)?.values() ?? [])].sort((left, right) => right.costUsd - left.costUsd || left.model.localeCompare(right.model));
67
102
  }
68
103
  const entries = [...byTask.values()].sort((left, right) => right.costUsd - left.costUsd || left.taskId.localeCompare(right.taskId));
69
104
  return { since: options.since, until: options.until, entries, unattributedCostUsd, truncated: options.truncated === true };
@@ -1,94 +0,0 @@
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
- }