@danypops/jittor 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +38 -13
  2. package/docs/COMPACTION_EFFECTIVENESS.md +34 -0
  3. package/docs/CONTEXT_SNAPSHOTS.md +53 -0
  4. package/docs/MODEL_CATALOG.md +43 -0
  5. package/docs/OTLP_EXPORT.md +63 -0
  6. package/docs/PROVIDER_RESEARCH.md +1 -1
  7. package/docs/TOKEN_MEASUREMENT.md +53 -0
  8. package/docs/USAGE_BACKFILL.md +35 -0
  9. package/package.json +4 -3
  10. package/src/{providers/anthropic-contracts.ts → anthropic/rate-limits.ts} +1 -1
  11. package/src/{adapters/artificial-analysis-direct-source.ts → artificial-analysis/benchmark-source.ts} +3 -3
  12. package/src/cli-commands/backfill.ts +51 -0
  13. package/src/cli-commands/benchmarks.ts +4 -4
  14. package/src/cli-commands/catalog.ts +123 -0
  15. package/src/cli-commands/compaction.ts +1 -1
  16. package/src/cli-commands/context.ts +77 -4
  17. package/src/cli-commands/export.ts +30 -0
  18. package/src/cli-commands/metrics.ts +8 -2
  19. package/src/cli-commands/op.ts +1 -1
  20. package/src/cli-commands/route-args.ts +2 -2
  21. package/src/cli-commands/router.ts +2 -2
  22. package/src/cli-commands/support.ts +2 -2
  23. package/src/cli.ts +54 -21
  24. package/src/{providers/codex-contracts.ts → codex/contracts.ts} +1 -1
  25. package/src/codex/source.ts +53 -0
  26. package/src/{providers/codex.ts → codex/telemetry.ts} +2 -2
  27. package/src/constants.ts +18 -0
  28. package/src/daemon.ts +53 -23
  29. package/src/{providers/google-vertex-budget-contracts.ts → google-vertex/budget-contracts.ts} +3 -3
  30. package/src/{providers/google-vertex-budget.ts → google-vertex/budget-telemetry.ts} +6 -6
  31. package/src/{providers/google-vertex-contracts.ts → google-vertex/failures.ts} +1 -1
  32. package/src/google-vertex/source.ts +35 -0
  33. package/src/index.ts +135 -55
  34. package/src/{adapters/lmarena-hf-source.ts → lmarena/benchmark-source.ts} +3 -3
  35. package/src/observability/budget.ts +14 -0
  36. package/src/observability/context-delta.ts +309 -0
  37. package/src/{domain → observability}/context-hub.ts +19 -2
  38. package/src/observability/context-snapshot-history.ts +281 -0
  39. package/src/{domain → observability}/context-telemetry.ts +232 -4
  40. package/src/observability/model-identity.ts +42 -0
  41. package/src/{domain → observability}/model-observation.ts +26 -11
  42. package/src/{ports/metric-store.ts → observability/store.ts} +2 -2
  43. package/src/{ports → observability}/telemetry-source.ts +2 -2
  44. package/src/observability/token-measurement.ts +194 -0
  45. package/src/observability/usage-import.ts +122 -0
  46. package/src/openai/token-counter.ts +63 -0
  47. package/src/{adapters/openrouter-benchmark-source.ts → openrouter/benchmark-source.ts} +3 -3
  48. package/src/{providers/openrouter-contracts.ts → openrouter/contracts.ts} +1 -1
  49. package/src/{adapters/openrouter-design-arena-source.ts → openrouter/design-arena-source.ts} +4 -4
  50. package/src/openrouter/source.ts +20 -0
  51. package/src/{providers/openrouter.ts → openrouter/telemetry.ts} +2 -2
  52. package/src/{domain → optimization/model-selection}/benchmark.ts +9 -27
  53. package/src/optimization/model-selection/catalog-store.ts +97 -0
  54. package/src/optimization/model-selection/catalog.ts +478 -0
  55. package/src/{ports/benchmark-controller.ts → optimization/model-selection/controller.ts} +1 -1
  56. package/src/{adapters/metric-benchmark-store.ts → optimization/model-selection/observation-store.ts} +5 -5
  57. package/src/{domain/model-ranking-service.ts → optimization/model-selection/ranker.ts} +6 -12
  58. package/src/{domain/model-ranking.ts → optimization/model-selection/ranking.ts} +8 -2
  59. package/src/{ports/benchmark-source.ts → optimization/model-selection/source.ts} +1 -1
  60. package/src/{ports/benchmark-store.ts → optimization/model-selection/store.ts} +1 -1
  61. package/src/{domain/codex-recovery.ts → optimization/recovery/codex.ts} +1 -1
  62. package/src/{ports/router-controller.ts → optimization/routing/controller.ts} +1 -1
  63. package/src/{policy.ts → optimization/routing/policy.ts} +4 -14
  64. package/src/{router.ts → optimization/routing/router.ts} +4 -10
  65. package/src/otlp/exporter.ts +354 -0
  66. package/src/pi/session-usage-source.ts +203 -0
  67. package/src/{session-identity-service.ts → sessions/identity.ts} +2 -2
  68. package/src/{operations/session-scope.ts → sessions/router-authorization.ts} +1 -1
  69. package/src/{db.ts → sqlite/database.ts} +14 -1
  70. package/src/{adapters/sqlite-metric-store.ts → sqlite/metric-store.ts} +4 -4
  71. package/src/{adapters/sqlite-session-identity-store.ts → sqlite/session-store.ts} +1 -1
  72. package/src/sqlite/usage-import-store.ts +133 -0
  73. package/src/telemetry-export/exporter.ts +84 -0
  74. package/src/{operations → vehicle}/benchmark-operations.ts +3 -3
  75. package/src/vehicle/catalog-operations.ts +10 -0
  76. package/src/{client.ts → vehicle/client.ts} +1 -1
  77. package/src/{operations → vehicle}/context-operations.ts +13 -5
  78. package/src/vehicle/export-operations.ts +12 -0
  79. package/src/{operations/metrics-operations.ts → vehicle/metric-operations.ts} +4 -4
  80. package/src/{operations → vehicle}/model-ranking-operations.ts +3 -3
  81. package/src/{vehicle-registration.ts → vehicle/registration.ts} +14 -4
  82. package/src/{operations/router-operations.ts → vehicle/routing-operations.ts} +4 -4
  83. package/src/{service.ts → vehicle/service.ts} +108 -26
  84. package/src/{operations/session-identity-operations.ts → vehicle/session-operations.ts} +3 -3
  85. package/src/vehicle/usage-import-operations.ts +10 -0
  86. package/src/providers/telemetry-sources.ts +0 -108
  87. /package/src/{providers/google-adc-auth.ts → google-vertex/auth.ts} +0 -0
  88. /package/src/{domain → observability}/metric.ts +0 -0
  89. /package/src/{domain → observability}/task-cost.ts +0 -0
  90. /package/src/{domain → observability}/task-focus.ts +0 -0
  91. /package/src/{domain → observability}/usage.ts +0 -0
  92. /package/src/{config.ts → optimization/routing/config.ts} +0 -0
  93. /package/src/{ports/session-identity-store.ts → sessions/store.ts} +0 -0
  94. /package/src/{operations/types.ts → vehicle/operation-types.ts} +0 -0
@@ -0,0 +1,354 @@
1
+ import {
2
+ OTLP_EXPORT_BATCH_SIZE,
3
+ OTLP_EXPORT_INTERVAL_MS,
4
+ OTLP_EXPORT_MAX_QUEUE_SIZE,
5
+ OTLP_EXPORT_MAX_RETRIES,
6
+ OTLP_EXPORT_TIMEOUT_MS,
7
+ } from "../constants.ts";
8
+ import type { MetricObservation } from "../observability/metric.ts";
9
+ import type { ObservationExporter, ObservationExportStatus } from "../telemetry-export/exporter.ts";
10
+
11
+ export const OTLP_GENAI_SEMANTIC_CONVENTIONS = "genai@46d43c8949afb53765a202e89f4534eeb75ca3fa+core-v1.44.0";
12
+
13
+ export interface OtlpMetricPoint {
14
+ name: string;
15
+ unit: string;
16
+ value: number;
17
+ observedAt: number;
18
+ attributes: Record<string, string | number | boolean>;
19
+ }
20
+
21
+ function bounded(value: unknown): string | undefined {
22
+ if (typeof value !== "string" || value.length < 1 || value.length > 200 || /\p{Cc}/u.test(value)) return undefined;
23
+ return value;
24
+ }
25
+
26
+ function boundedIdentity(value: unknown): string | undefined {
27
+ const result = bounded(value);
28
+ return result && /^[a-zA-Z0-9@~][a-zA-Z0-9@~._:+/-]*$/.test(result) && !result.includes("//") ? result : undefined;
29
+ }
30
+
31
+ function providerName(value: string): string {
32
+ if (value === "openai-codex") return "openai";
33
+ if (value === "google" || value === "google-gemini") return "gcp.gemini";
34
+ if (value === "google-vertex" || value === "anthropic-vertex") return "gcp.vertex_ai";
35
+ return value;
36
+ }
37
+
38
+ function identity(observation: MetricObservation): { provider?: string; model?: string } {
39
+ const provider = boundedIdentity(observation.attributes?.provider);
40
+ const model = boundedIdentity(observation.attributes?.model);
41
+ if (provider || model) return { ...(provider ? { provider } : {}), ...(model ? { model } : {}) };
42
+ const separator = observation.scope.includes(":") ? ":" : observation.scope.includes("/") ? "/" : null;
43
+ if (!separator) return {};
44
+ const [scopeProvider, ...modelParts] = observation.scope.split(separator);
45
+ const scopeModel = modelParts.join(separator);
46
+ return {
47
+ ...(boundedIdentity(scopeProvider) ? { provider: scopeProvider } : {}),
48
+ ...(boundedIdentity(scopeModel) ? { model: scopeModel } : {}),
49
+ };
50
+ }
51
+
52
+ const CUSTOM_ATTRIBUTES: Record<string, string> = {
53
+ mechanism: "jittor.compaction.mechanism",
54
+ reason: "jittor.compaction.reason",
55
+ willRetry: "jittor.compaction.will_retry",
56
+ threshold: "jittor.compaction.regrowth_threshold",
57
+ preContextTokens: "jittor.compaction.pre_context_tokens",
58
+ postContextTokens: "jittor.compaction.post_context_tokens",
59
+ summaryTokens: "jittor.compaction.summary_tokens",
60
+ turnsSinceCompaction: "jittor.compaction.turns_since",
61
+ elapsedSinceCompactionMs: "jittor.compaction.elapsed_since_ms",
62
+ contextTokens: "jittor.context.tokens",
63
+ pressure: "jittor.routing.pressure",
64
+ action: "jittor.routing.action",
65
+ };
66
+
67
+ const CUSTOM_ATTRIBUTE_VALUES: Record<string, ReadonlySet<string>> = {
68
+ mechanism: new Set(["pi-native", "provider-side", "extension"]),
69
+ reason: new Set(["manual", "threshold", "overflow"]),
70
+ action: new Set(["continue", "warn", "throttle", "downgrade-thinking", "handoff", "halt"]),
71
+ };
72
+
73
+ function safeCustomAttributes(input: Record<string, unknown> | undefined): Record<string, string | number | boolean> {
74
+ const result: Record<string, string | number | boolean> = {};
75
+ if (!input) return result;
76
+ for (const [source, target] of Object.entries(CUSTOM_ATTRIBUTES)) {
77
+ const value = input[source];
78
+ if (typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value))) result[target] = value;
79
+ else {
80
+ const text = bounded(value);
81
+ if (text && CUSTOM_ATTRIBUTE_VALUES[source]?.has(text)) result[target] = text;
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+
87
+ function safeName(value: string): string {
88
+ return value
89
+ .toLowerCase()
90
+ .replace(/[^a-z0-9_.]+/g, "_")
91
+ .slice(0, 100);
92
+ }
93
+
94
+ export function mapObservationToOtlp(observation: MetricObservation): OtlpMetricPoint | null {
95
+ if (typeof observation.value !== "number" || !Number.isFinite(observation.value) || observation.value < 0) return null;
96
+ const ids = identity(observation);
97
+ const requestedOperation = bounded(observation.attributes?.operation);
98
+ const operation =
99
+ requestedOperation &&
100
+ new Set(["chat", "generate_content", "text_completion", "embeddings", "invoke_agent", "invoke_workflow", "execute_tool"]).has(
101
+ requestedOperation,
102
+ )
103
+ ? requestedOperation
104
+ : "chat";
105
+ const attributes: Record<string, string | number | boolean> = {
106
+ "gen_ai.operation.name": operation,
107
+ ...safeCustomAttributes(observation.attributes),
108
+ };
109
+ if (ids.provider) attributes["gen_ai.provider.name"] = providerName(ids.provider);
110
+ if (ids.model) attributes["gen_ai.request.model"] = ids.model;
111
+ const thinking = bounded(observation.attributes?.thinking);
112
+ if (thinking && new Set(["off", "none", "minimal", "low", "medium", "high", "xhigh", "max"]).has(thinking))
113
+ attributes["gen_ai.request.reasoning.level"] = thinking;
114
+ const runId = bounded(observation.attributes?.runId) ?? bounded(observation.attributes?.importIdentity);
115
+ if (runId && /^[a-zA-Z0-9_-]+$/.test(runId)) attributes["jittor.run.id"] = runId;
116
+ const conversationId = bounded(observation.attributes?.sessionId) ?? bounded(observation.attributes?.session_id);
117
+ if (conversationId && conversationId.length >= 32 && /^[a-zA-Z0-9_-]+$/.test(conversationId))
118
+ attributes["gen_ai.conversation.id"] = conversationId;
119
+
120
+ let name: string;
121
+ let unit: string;
122
+ let value = observation.value;
123
+ if (observation.metric === "input-tokens" || observation.metric === "output-tokens") {
124
+ name = "gen_ai.client.token.usage";
125
+ unit = "{token}";
126
+ attributes["gen_ai.token.type"] = observation.metric === "input-tokens" ? "input" : "output";
127
+ } else if (observation.metric === "cache-read-tokens" || observation.metric === "cache-write-tokens") {
128
+ name = "jittor.gen_ai.cache.token.usage";
129
+ unit = "{token}";
130
+ attributes["jittor.token.type"] = observation.metric === "cache-read-tokens" ? "cache_read" : "cache_write";
131
+ } else if (observation.metric === "reasoning-tokens") {
132
+ name = "jittor.gen_ai.reasoning.token.usage";
133
+ unit = "{token}";
134
+ } else if (observation.metric === "wall-latency") {
135
+ name = "gen_ai.client.operation.duration";
136
+ unit = "s";
137
+ value /= 1_000;
138
+ } else if (observation.metric === "ttft") {
139
+ name = "gen_ai.client.operation.time_to_first_chunk";
140
+ unit = "s";
141
+ value /= 1_000;
142
+ } else if (observation.metric === "output-throughput") {
143
+ name = "jittor.gen_ai.output.throughput";
144
+ unit = "{token}/s";
145
+ } else {
146
+ name = `jittor.${safeName(observation.source)}.${safeName(observation.metric)}`;
147
+ unit = observation.unit;
148
+ }
149
+ if (observation.metric === "failure" && observation.value > 0) attributes["error.type"] = "_OTHER";
150
+ return { name, unit, value, observedAt: observation.observedAt, attributes };
151
+ }
152
+
153
+ function otlpAttribute(key: string, value: string | number | boolean): object {
154
+ return {
155
+ key,
156
+ value:
157
+ typeof value === "string"
158
+ ? { stringValue: value }
159
+ : typeof value === "boolean"
160
+ ? { boolValue: value }
161
+ : Number.isSafeInteger(value)
162
+ ? { intValue: String(value) }
163
+ : { doubleValue: value },
164
+ };
165
+ }
166
+
167
+ export function buildOtlpMetricsRequest(points: OtlpMetricPoint[]): object {
168
+ const grouped = new Map<string, OtlpMetricPoint[]>();
169
+ for (const point of points) {
170
+ const key = `${point.name}\0${point.unit}`;
171
+ grouped.set(key, [...(grouped.get(key) ?? []), point]);
172
+ }
173
+ return {
174
+ resourceMetrics: [
175
+ {
176
+ resource: {
177
+ attributes: [otlpAttribute("service.name", "jittor"), otlpAttribute("jittor.semconv.version", OTLP_GENAI_SEMANTIC_CONVENTIONS)],
178
+ },
179
+ scopeMetrics: [
180
+ {
181
+ scope: { name: "@danypops/jittor", version: OTLP_GENAI_SEMANTIC_CONVENTIONS },
182
+ metrics: [...grouped.values()].map((values) => ({
183
+ name: values[0]!.name,
184
+ unit: values[0]!.unit,
185
+ histogram: {
186
+ aggregationTemporality: 1,
187
+ dataPoints: values.map((point) => ({
188
+ timeUnixNano: String(point.observedAt * 1_000_000),
189
+ count: "1",
190
+ sum: point.value,
191
+ min: point.value,
192
+ max: point.value,
193
+ explicitBounds: [],
194
+ bucketCounts: ["1"],
195
+ attributes: Object.entries(point.attributes).map(([key, value]) => otlpAttribute(key, value)),
196
+ })),
197
+ },
198
+ })),
199
+ },
200
+ ],
201
+ },
202
+ ],
203
+ };
204
+ }
205
+
206
+ export type OtlpTransport = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
207
+
208
+ export interface OtlpObservationExporterOptions {
209
+ endpoint: string;
210
+ headers?: Record<string, string>;
211
+ transport?: OtlpTransport;
212
+ clock?: () => number;
213
+ maxQueueSize?: number;
214
+ batchSize?: number;
215
+ intervalMs?: number;
216
+ timeoutMs?: number;
217
+ maxRetries?: number;
218
+ }
219
+
220
+ export class OtlpObservationExporter implements ObservationExporter {
221
+ private readonly endpoint: string;
222
+ private readonly headers: Record<string, string>;
223
+ private readonly transport: OtlpTransport;
224
+ private readonly clock: () => number;
225
+ private readonly maxQueueSize: number;
226
+ private readonly batchSize: number;
227
+ private readonly timeoutMs: number;
228
+ private readonly maxRetries: number;
229
+ private readonly queue: OtlpMetricPoint[] = [];
230
+ private timer: ReturnType<typeof setInterval> | undefined;
231
+ private flushing: Promise<void> | undefined;
232
+ private closed = false;
233
+ private exported = 0;
234
+ private dropped = 0;
235
+ private failures = 0;
236
+ private lastSuccessAt: number | null = null;
237
+ private lastFailureAt: number | null = null;
238
+
239
+ constructor(options: OtlpObservationExporterOptions) {
240
+ const url = new URL(options.endpoint);
241
+ if (url.protocol !== "https:" && url.hostname !== "127.0.0.1" && url.hostname !== "localhost")
242
+ throw new Error("OTLP endpoint must use HTTPS or loopback HTTP");
243
+ this.endpoint = url.toString();
244
+ this.headers = { ...(options.headers ?? {}) };
245
+ this.transport = options.transport ?? fetch;
246
+ this.clock = options.clock ?? Date.now;
247
+ this.maxQueueSize = Math.max(1, options.maxQueueSize ?? OTLP_EXPORT_MAX_QUEUE_SIZE);
248
+ this.batchSize = Math.max(1, options.batchSize ?? OTLP_EXPORT_BATCH_SIZE);
249
+ this.timeoutMs = Math.max(1, options.timeoutMs ?? OTLP_EXPORT_TIMEOUT_MS);
250
+ this.maxRetries = Math.max(0, options.maxRetries ?? OTLP_EXPORT_MAX_RETRIES);
251
+ this.timer = setInterval(() => void this.flush(), Math.max(10, options.intervalMs ?? OTLP_EXPORT_INTERVAL_MS));
252
+ this.timer.unref?.();
253
+ }
254
+
255
+ enqueue(observation: MetricObservation): void {
256
+ const point = mapObservationToOtlp(observation);
257
+ if (!point) return;
258
+ if (this.closed) {
259
+ this.dropped += 1;
260
+ return;
261
+ }
262
+ if (this.queue.length >= this.maxQueueSize) {
263
+ this.queue.shift();
264
+ this.dropped += 1;
265
+ }
266
+ this.queue.push(point);
267
+ if (this.queue.length >= this.batchSize) queueMicrotask(() => void this.flush());
268
+ }
269
+
270
+ async flush(): Promise<void> {
271
+ if (this.flushing) return this.flushing;
272
+ if (this.queue.length === 0) return;
273
+ this.flushing = (async () => {
274
+ while (this.queue.length > 0) await this.send(this.queue.splice(0, this.batchSize));
275
+ })().finally(() => {
276
+ this.flushing = undefined;
277
+ });
278
+ return this.flushing;
279
+ }
280
+
281
+ private async send(points: OtlpMetricPoint[]): Promise<void> {
282
+ const body = JSON.stringify(buildOtlpMetricsRequest(points));
283
+ for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
284
+ const controller = new AbortController();
285
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
286
+ try {
287
+ const response = await this.transport(this.endpoint, {
288
+ method: "POST",
289
+ headers: { "content-type": "application/json", ...this.headers },
290
+ body,
291
+ signal: controller.signal,
292
+ });
293
+ if (!response.ok) throw new Error("OTLP collector rejected export");
294
+ this.exported += points.length;
295
+ this.lastSuccessAt = this.clock();
296
+ return;
297
+ } catch {
298
+ if (attempt < this.maxRetries) continue;
299
+ this.failures += 1;
300
+ this.dropped += points.length;
301
+ this.lastFailureAt = this.clock();
302
+ } finally {
303
+ clearTimeout(timeout);
304
+ }
305
+ }
306
+ }
307
+
308
+ status(): ObservationExportStatus {
309
+ return {
310
+ enabled: true,
311
+ semanticConventions: OTLP_GENAI_SEMANTIC_CONVENTIONS,
312
+ queued: this.queue.length,
313
+ dropped: this.dropped,
314
+ exported: this.exported,
315
+ failures: this.failures,
316
+ lastSuccessAt: this.lastSuccessAt,
317
+ lastFailureAt: this.lastFailureAt,
318
+ };
319
+ }
320
+
321
+ async shutdown(): Promise<void> {
322
+ this.closed = true;
323
+ if (this.timer) clearInterval(this.timer);
324
+ this.timer = undefined;
325
+ while (this.queue.length > 0) await this.flush();
326
+ if (this.flushing) await this.flushing;
327
+ }
328
+ }
329
+
330
+ function parseHeaders(raw: string | undefined): Record<string, string> {
331
+ if (!raw) return {};
332
+ const result: Record<string, string> = {};
333
+ for (const pair of raw.split(",")) {
334
+ const separator = pair.indexOf("=");
335
+ if (separator < 1) continue;
336
+ const key = pair.slice(0, separator).trim().toLowerCase();
337
+ if (!/^[a-z0-9-]+$/.test(key)) continue;
338
+ try {
339
+ result[key] = decodeURIComponent(pair.slice(separator + 1).trim());
340
+ } catch {}
341
+ }
342
+ return result;
343
+ }
344
+
345
+ export function otlpExporterFromEnvironment(env: Record<string, string | undefined> = process.env): ObservationExporter | null {
346
+ let endpoint = env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT;
347
+ if (!endpoint && env.OTEL_EXPORTER_OTLP_ENDPOINT)
348
+ endpoint = new URL(
349
+ "v1/metrics",
350
+ env.OTEL_EXPORTER_OTLP_ENDPOINT.endsWith("/") ? env.OTEL_EXPORTER_OTLP_ENDPOINT : `${env.OTEL_EXPORTER_OTLP_ENDPOINT}/`,
351
+ ).toString();
352
+ if (!endpoint) return null;
353
+ return new OtlpObservationExporter({ endpoint, headers: parseHeaders(env.OTEL_EXPORTER_OTLP_HEADERS) });
354
+ }
@@ -0,0 +1,203 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import {
3
+ PI_USAGE_IMPORT_MAX_DURATION_MS,
4
+ PI_USAGE_IMPORT_MAX_ENTRIES,
5
+ PI_USAGE_IMPORT_MAX_FILE_BYTES,
6
+ PI_USAGE_IMPORT_MAX_FILES,
7
+ PI_USAGE_IMPORT_MAX_RECORDS,
8
+ PI_USAGE_IMPORT_MAX_TOTAL_BYTES,
9
+ } from "../constants.ts";
10
+ import type { HistoricalUsageRecord, HistoricalUsageScan, HistoricalUsageSource } from "../observability/usage-import.ts";
11
+
12
+ interface SessionState {
13
+ provider: string | null;
14
+ model: string | null;
15
+ thinking: string | null;
16
+ }
17
+
18
+ async function opaqueIdentity(...parts: string[]): Promise<string> {
19
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(parts.join("\0")));
20
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
21
+ }
22
+
23
+ function object(value: unknown): Record<string, unknown> | null {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
25
+ }
26
+
27
+ function nonnegativeInteger(value: unknown): number | null {
28
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
29
+ }
30
+
31
+ function nonnegativeNumber(value: unknown): number | null {
32
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
33
+ }
34
+
35
+ function safeIdentity(value: unknown): string | null {
36
+ if (typeof value !== "string" || value.length < 1 || value.length > 200 || /\p{Cc}/u.test(value)) return null;
37
+ return value;
38
+ }
39
+
40
+ function timestamp(entry: Record<string, unknown>, message?: Record<string, unknown>): number | null {
41
+ const messageTime = message?.timestamp;
42
+ if (typeof messageTime === "number" && Number.isSafeInteger(messageTime) && messageTime >= 0) return messageTime;
43
+ if (typeof entry.timestamp !== "string") return null;
44
+ const value = Date.parse(entry.timestamp);
45
+ return Number.isFinite(value) && value >= 0 ? value : null;
46
+ }
47
+
48
+ function usageRecord(
49
+ usageValue: unknown,
50
+ entry: Record<string, unknown>,
51
+ state: SessionState,
52
+ sessionId: string,
53
+ entryId: string,
54
+ message?: Record<string, unknown>,
55
+ ): Promise<HistoricalUsageRecord | null> | null {
56
+ const usage = object(usageValue);
57
+ const provider = safeIdentity(message?.provider) ?? state.provider;
58
+ const model = safeIdentity(message?.model) ?? state.model;
59
+ const observedAt = timestamp(entry, message);
60
+ if (!usage || !provider || !model || observedAt === null) return null;
61
+ const inputTokens = nonnegativeInteger(usage.input);
62
+ const outputTokens = nonnegativeInteger(usage.output);
63
+ const cacheReadTokens = nonnegativeInteger(usage.cacheRead);
64
+ const cacheWriteTokens = nonnegativeInteger(usage.cacheWrite);
65
+ const cost = object(usage.cost);
66
+ const costUsd = nonnegativeNumber(cost?.total);
67
+ if (inputTokens === null || outputTokens === null || cacheReadTokens === null || cacheWriteTokens === null || costUsd === null)
68
+ return null;
69
+ return opaqueIdentity(sessionId, entryId).then((identity) => ({
70
+ identity,
71
+ observedAt,
72
+ provider,
73
+ model,
74
+ thinking: state.thinking,
75
+ inputTokens,
76
+ outputTokens,
77
+ cacheReadTokens,
78
+ cacheWriteTokens,
79
+ costUsd,
80
+ }));
81
+ }
82
+
83
+ export interface PiSessionUsageSourceOptions {
84
+ maxFiles?: number;
85
+ maxFileBytes?: number;
86
+ maxTotalBytes?: number;
87
+ maxEntries?: number;
88
+ maxRecords?: number;
89
+ maxDurationMs?: number;
90
+ clock?: () => number;
91
+ }
92
+
93
+ /** Reads Pi's documented v1-v3 JSONL session format and immediately discards all content fields. */
94
+ export class PiSessionUsageSource implements HistoricalUsageSource {
95
+ private readonly options: Required<PiSessionUsageSourceOptions>;
96
+
97
+ constructor(
98
+ private readonly sessionsRoot: string,
99
+ options: PiSessionUsageSourceOptions = {},
100
+ ) {
101
+ this.options = {
102
+ maxFiles: options.maxFiles ?? PI_USAGE_IMPORT_MAX_FILES,
103
+ maxFileBytes: options.maxFileBytes ?? PI_USAGE_IMPORT_MAX_FILE_BYTES,
104
+ maxTotalBytes: options.maxTotalBytes ?? PI_USAGE_IMPORT_MAX_TOTAL_BYTES,
105
+ maxEntries: options.maxEntries ?? PI_USAGE_IMPORT_MAX_ENTRIES,
106
+ maxRecords: options.maxRecords ?? PI_USAGE_IMPORT_MAX_RECORDS,
107
+ maxDurationMs: options.maxDurationMs ?? PI_USAGE_IMPORT_MAX_DURATION_MS,
108
+ clock: options.clock ?? Date.now,
109
+ };
110
+ }
111
+
112
+ async scan(canceled: () => boolean): Promise<HistoricalUsageScan> {
113
+ const startedAt = this.options.clock();
114
+ let names: string[];
115
+ try {
116
+ names = (await readdir(this.sessionsRoot, { recursive: true, encoding: "utf8" })).filter((name) => name.endsWith(".jsonl")).sort();
117
+ } catch {
118
+ names = [];
119
+ }
120
+ const records: HistoricalUsageRecord[] = [];
121
+ let filesScanned = 0;
122
+ let entriesScanned = 0;
123
+ let bytesScanned = 0;
124
+ let malformedEntries = 0;
125
+ let truncated = names.length > this.options.maxFiles;
126
+ let cursor: string | null = null;
127
+ for (const name of names.slice(0, this.options.maxFiles)) {
128
+ if (canceled() || this.options.clock() - startedAt > this.options.maxDurationMs) {
129
+ truncated = true;
130
+ break;
131
+ }
132
+ const path = `${this.sessionsRoot}/${name}`;
133
+ const size = (await stat(path)).size;
134
+ if (size > this.options.maxFileBytes || bytesScanned + size > this.options.maxTotalBytes) {
135
+ truncated = true;
136
+ continue;
137
+ }
138
+ const lines = (await readFile(path, "utf8")).split("\n");
139
+ bytesScanned += size;
140
+ filesScanned += 1;
141
+ let sessionId: string | null = null;
142
+ const stateById = new Map<string | null, SessionState>([[null, { provider: null, model: null, thinking: null }]]);
143
+ for (const line of lines) {
144
+ if (line.trim().length === 0) continue;
145
+ if (entriesScanned >= this.options.maxEntries || records.length >= this.options.maxRecords || canceled()) {
146
+ truncated = true;
147
+ break;
148
+ }
149
+ entriesScanned += 1;
150
+ let entry: Record<string, unknown> | null;
151
+ try {
152
+ entry = object(JSON.parse(line));
153
+ } catch {
154
+ entry = null;
155
+ }
156
+ if (!entry) {
157
+ malformedEntries += 1;
158
+ continue;
159
+ }
160
+ if (entry.type === "session") {
161
+ sessionId = safeIdentity(entry.id);
162
+ if (sessionId) cursor = await opaqueIdentity(sessionId);
163
+ continue;
164
+ }
165
+ const entryId = safeIdentity(entry.id);
166
+ const parentId = entry.parentId === null ? null : safeIdentity(entry.parentId);
167
+ if (!sessionId || !entryId || (entry.parentId !== null && parentId === null)) {
168
+ malformedEntries += 1;
169
+ continue;
170
+ }
171
+ const inherited = stateById.get(parentId) ?? { provider: null, model: null, thinking: null };
172
+ const state = { ...inherited };
173
+ if (entry.type === "model_change") {
174
+ state.provider = safeIdentity(entry.provider);
175
+ state.model = safeIdentity(entry.modelId);
176
+ } else if (entry.type === "thinking_level_change") state.thinking = safeIdentity(entry.thinkingLevel);
177
+ stateById.set(entryId, state);
178
+ const message = entry.type === "message" ? object(entry.message) : undefined;
179
+ const candidate =
180
+ message?.role === "assistant"
181
+ ? usageRecord(message.usage, entry, state, sessionId, entryId, message)
182
+ : entry.type === "compaction" || entry.type === "branch_summary"
183
+ ? usageRecord(entry.usage, entry, state, sessionId, entryId)
184
+ : null;
185
+ if (candidate) {
186
+ const resolved = await candidate;
187
+ if (resolved) records.push(resolved);
188
+ }
189
+ }
190
+ if (truncated) break;
191
+ }
192
+ return {
193
+ records,
194
+ filesScanned,
195
+ entriesScanned,
196
+ bytesScanned,
197
+ malformedEntries,
198
+ truncated,
199
+ canceled: canceled(),
200
+ cursor,
201
+ };
202
+ }
203
+ }
@@ -4,7 +4,7 @@ import {
4
4
  releaseSessionIdentity,
5
5
  verifySessionSecret,
6
6
  } from "@danypops/vehicle-server/session-identity";
7
- import type { SessionIdentityStore } from "./ports/session-identity-store.ts";
7
+ import type { SessionIdentityStore } from "./store.ts";
8
8
 
9
9
  export interface RegisterSessionIdentityResult {
10
10
  sessionId: string;
@@ -21,7 +21,7 @@ function assertValidSessionId(sessionId: string): void {
21
21
  /**
22
22
  * Wraps daemon-kit's storage-agnostic session-identity primitive against Jittor's own
23
23
  * SQLite-backed store, and enforces it at the one place a caller-supplied session_id is
24
- * behavior-affecting: mutating router.* operations (see ports/router-controller.ts).
24
+ * behavior-affecting: mutating router.* operations (see optimization/routing/controller.ts).
25
25
  *
26
26
  * Opt-in armor, not a breaking migration: a session_id that was never registered mutates
27
27
  * exactly as before (undefined sessionId included, since that maps to the router's legacy
@@ -1,4 +1,4 @@
1
- import type { SessionIdentity } from "../session-identity-service.ts";
1
+ import type { SessionIdentity } from "./identity.ts";
2
2
 
3
3
  /** Shared input parsing for every operation that accepts an optional session_id/session_secret pair. */
4
4
  export function routerSessionId(input: Record<string, unknown>): string | undefined {
@@ -1,6 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import { openSqliteWithPragmas } from "@danypops/vehicle-server/storage";
3
- import { SQLITE_BUSY_TIMEOUT_MS } from "./constants.ts";
3
+ import { SQLITE_BUSY_TIMEOUT_MS } from "../constants.ts";
4
4
 
5
5
  const INITIAL_SCHEMA = `
6
6
  CREATE TABLE metric_observations (
@@ -30,6 +30,18 @@ CREATE INDEX session_identities_last_seen_idx
30
30
  ON session_identities(last_seen_at);
31
31
  `;
32
32
 
33
+ const USAGE_IMPORT_SCHEMA = `
34
+ CREATE TABLE usage_import_identities (
35
+ identity TEXT PRIMARY KEY,
36
+ imported_at INTEGER NOT NULL CHECK(imported_at >= 0)
37
+ );
38
+ CREATE TABLE usage_import_state (
39
+ id INTEGER PRIMARY KEY CHECK(id = 1),
40
+ status TEXT NOT NULL CHECK(json_valid(status)),
41
+ updated_at INTEGER NOT NULL CHECK(updated_at >= 0)
42
+ );
43
+ `;
44
+
33
45
  /**
34
46
  * Delegates bootstrap (pragmas, migration engine) to `@danypops/vehicle-server/storage`, which
35
47
  * generalizes the byte-identical pragma/PRAGMA-user_version skeleton jittor's own db.ts used to
@@ -42,6 +54,7 @@ export function openJittorDb(path: string): Database {
42
54
  migrations: [
43
55
  { version: 1, up: (db) => db.exec(INITIAL_SCHEMA) },
44
56
  { version: 2, up: (db) => db.exec(SESSION_IDENTITY_SCHEMA) },
57
+ { version: 3, up: (db) => db.exec(USAGE_IMPORT_SCHEMA) },
45
58
  ],
46
59
  });
47
60
  }
@@ -1,9 +1,9 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import { DEFAULT_QUERY_LIMIT, MAX_QUERY_LIMIT, USAGE_AGGREGATE_MAX_ROWS } from "../constants.ts";
3
- import type { MetricObservation, MetricQuery, StoredMetricObservation } from "../domain/metric.ts";
4
- import { validateMetricObservation } from "../domain/metric.ts";
5
- import type { UsageAggregateRow } from "../domain/usage.ts";
6
- import type { DistinctScopesFilter, MetricStore, UsageAggregateFilter } from "../ports/metric-store.ts";
3
+ import type { MetricObservation, MetricQuery, StoredMetricObservation } from "../observability/metric.ts";
4
+ import { validateMetricObservation } from "../observability/metric.ts";
5
+ import type { DistinctScopesFilter, MetricStore, UsageAggregateFilter } from "../observability/store.ts";
6
+ import type { UsageAggregateRow } from "../observability/usage.ts";
7
7
 
8
8
  interface MetricRow {
9
9
  id: number;
@@ -1,6 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import { SESSION_IDENTITY_MAX_ROWS } from "../constants.ts";
3
- import type { SessionIdentityRecord, SessionIdentityStore } from "../ports/session-identity-store.ts";
3
+ import type { SessionIdentityRecord, SessionIdentityStore } from "../sessions/store.ts";
4
4
 
5
5
  export class SQLiteSessionIdentityStore implements SessionIdentityStore {
6
6
  constructor(private readonly db: Database) {}