@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.
- package/README.md +62 -7
- package/docs/BENCHMARK_SOURCES.md +32 -0
- package/docs/OUTPUT_CHANNELS.md +31 -0
- package/docs/PROVIDER_RESEARCH.md +57 -0
- package/extension/src/benchmark-tui.ts +105 -0
- package/extension/src/footer.ts +68 -12
- package/extension/src/index.ts +263 -36
- package/extension/src/tui.ts +61 -9
- package/extension/src/usage.ts +165 -47
- package/package.json +5 -1
- package/src/adapters/metric-benchmark-store.ts +99 -0
- package/src/adapters/openrouter-benchmark-index-source.ts +94 -0
- package/src/adapters/openrouter-benchmark-source.ts +109 -0
- package/src/adapters/sqlite-metric-store.ts +43 -2
- package/src/cli.ts +660 -9
- package/src/client.ts +12 -44
- package/src/constants.ts +69 -5
- package/src/daemon.ts +65 -43
- package/src/db.ts +13 -30
- package/src/domain/benchmark.ts +264 -0
- package/src/domain/context-telemetry.ts +31 -0
- package/src/domain/metric.ts +46 -5
- package/src/domain/model-observation.ts +203 -0
- package/src/domain/model-ranking-service.ts +41 -0
- package/src/domain/model-ranking.ts +232 -0
- package/src/domain/task-cost.ts +70 -0
- package/src/domain/task-focus.ts +65 -0
- package/src/domain/usage.ts +134 -33
- package/src/log.ts +28 -0
- package/src/ports/benchmark-controller.ts +11 -0
- package/src/ports/benchmark-source.ts +7 -0
- package/src/ports/benchmark-store.ts +7 -0
- package/src/ports/metric-store.ts +30 -0
- package/src/ports/router-controller.ts +1 -0
- package/src/providers/anthropic-contracts.ts +127 -0
- package/src/providers/google-adc-auth.ts +63 -0
- package/src/providers/google-vertex-budget-contracts.ts +181 -0
- package/src/providers/google-vertex-budget.ts +127 -0
- package/src/providers/google-vertex-contracts.ts +116 -0
- package/src/providers/telemetry-sources.ts +35 -0
- package/src/router.ts +12 -0
- package/src/service.ts +132 -17
- package/src/state.ts +31 -57
- package/src/version.ts +2 -14
package/src/client.ts
CHANGED
|
@@ -1,51 +1,19 @@
|
|
|
1
|
+
import { AuthenticatedRpcClient, type FetchTransport } from "@danypops/daemon-kit/rpc-client";
|
|
1
2
|
import type { OperationInputs, OperationName, OperationOutputs } from "./service.ts";
|
|
2
3
|
import { ensureAuthToken, readDaemonHandle, resolveJittorPaths, type JittorPaths } from "./state.ts";
|
|
3
4
|
|
|
4
|
-
export type FetchTransport
|
|
5
|
+
export type { FetchTransport };
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
headers: { authorization: `Bearer ${this.token}`, "content-type": "application/json" },
|
|
17
|
-
body: JSON.stringify({ op: operation, input }),
|
|
18
|
-
}));
|
|
19
|
-
const body = await response.json() as { result?: OperationOutputs[Name]; error?: string };
|
|
20
|
-
if (!response.ok) throw new Error(body.error ?? `Jittor operation failed with HTTP ${response.status}`);
|
|
21
|
-
return body.result as OperationOutputs[Name];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async operations(): Promise<OperationName[]> {
|
|
25
|
-
const response = await this.transport(new Request(`${this.baseUrl}/api/v1/ops`, {
|
|
26
|
-
headers: { authorization: `Bearer ${this.token}` },
|
|
27
|
-
}));
|
|
28
|
-
const body = await response.json() as { operations?: OperationName[]; error?: string };
|
|
29
|
-
if (!response.ok) throw new Error(body.error ?? `Jittor discovery failed with HTTP ${response.status}`);
|
|
30
|
-
return body.operations ?? [];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async ready(): Promise<boolean> {
|
|
34
|
-
const response = await this.transport(new Request(`${this.baseUrl}/ready`, {
|
|
35
|
-
headers: { authorization: `Bearer ${this.token}` },
|
|
36
|
-
}));
|
|
37
|
-
if (response.status === 503) return false;
|
|
38
|
-
if (!response.ok) throw new Error(`Jittor readiness check failed with HTTP ${response.status}`);
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async health(): Promise<{ ok: true; version: string }> {
|
|
43
|
-
const response = await this.transport(new Request(`${this.baseUrl}/health`, {
|
|
44
|
-
headers: { authorization: `Bearer ${this.token}` },
|
|
45
|
-
}));
|
|
46
|
-
const body = await response.json() as { ok?: boolean; version?: string; error?: string };
|
|
47
|
-
if (!response.ok || body.ok !== true || typeof body.version !== "string") throw new Error(body.error ?? "Jittor health check failed");
|
|
48
|
-
return { ok: true, version: body.version };
|
|
7
|
+
/**
|
|
8
|
+
* Jittor's typed authenticated RPC client, now a thin named subclass of
|
|
9
|
+
* `@danypops/daemon-kit/rpc-client`'s `AuthenticatedRpcClient` -- the shared substrate factored
|
|
10
|
+
* out after jittor's own client.ts and web-spider-daemon's were found byte-identical (see
|
|
11
|
+
* daemon-kit's README). Keeps the old 3-positional-argument constructor so every existing call
|
|
12
|
+
* site is untouched by this migration.
|
|
13
|
+
*/
|
|
14
|
+
export class JittorClient extends AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs> {
|
|
15
|
+
constructor(baseUrl: string, token: string, transport: FetchTransport = fetch) {
|
|
16
|
+
super(baseUrl, token, { label: "Jittor", transport });
|
|
49
17
|
}
|
|
50
18
|
}
|
|
51
19
|
|
package/src/constants.ts
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
|
-
export const SQLITE_SCHEMA_VERSION = 1;
|
|
2
1
|
export const SQLITE_BUSY_TIMEOUT_MS = 5_000;
|
|
3
2
|
export const DEFAULT_QUERY_LIMIT = 1_000;
|
|
4
3
|
export const MAX_QUERY_LIMIT = 10_000;
|
|
5
4
|
export const SERVICE_MAX_BODY_BYTES = 1_048_576;
|
|
5
|
+
export const SERVICE_MAX_RESPONSE_BYTES = 4_194_304;
|
|
6
|
+
export const METRIC_IDENTITY_MAX_CHARACTERS = 160;
|
|
7
|
+
export const METRIC_ATTRIBUTES_MAX_SERIALIZED_CHARACTERS = 16_384;
|
|
8
|
+
export const METRIC_ATTRIBUTES_MAX_DEPTH = 12;
|
|
9
|
+
export const BENCHMARK_IDENTITY_MAX_CHARACTERS = 160;
|
|
10
|
+
export const BENCHMARK_MAX_TEXT_CHARACTERS = 2_048;
|
|
11
|
+
export const BENCHMARK_MAX_MODELS_PER_SOURCE = 250;
|
|
12
|
+
export const BENCHMARK_MAX_OBSERVATIONS_PER_SNAPSHOT = 2_000;
|
|
13
|
+
export const BENCHMARK_STORE_QUERY_LIMIT = (BENCHMARK_MAX_OBSERVATIONS_PER_SNAPSHOT * 2) + 1;
|
|
14
|
+
export const BENCHMARK_DEFAULT_QUERY_LIMIT = 100;
|
|
15
|
+
export const BENCHMARK_TUI_MAX_CANDIDATES = 20;
|
|
16
|
+
export const BENCHMARK_TUI_MAX_PROVENANCE_PER_CANDIDATE = 2;
|
|
17
|
+
export const BENCHMARK_MAX_QUERY_LIMIT = 500;
|
|
18
|
+
export const BENCHMARK_SOURCE_MAX_RESPONSE_BYTES = 2_097_152;
|
|
19
|
+
export const BENCHMARK_SOURCE_MAX_TOTAL_RESPONSE_BYTES = 6_291_456;
|
|
20
|
+
export const BENCHMARK_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1_000;
|
|
21
|
+
export const MODEL_OBSERVATION_IDENTITY_MAX_CHARACTERS = 160;
|
|
22
|
+
export const MODEL_AGGREGATE_MAX_ROWS = 10_000;
|
|
23
|
+
export const MODEL_AGGREGATE_MAX_GROUPS = 500;
|
|
24
|
+
export const MODEL_RANKING_MAX_SOURCES = 4;
|
|
25
|
+
export const MODEL_RANKING_DEFAULT_QUALITY_WEIGHT = 3;
|
|
26
|
+
export const MODEL_RANKING_DEFAULT_COST_WEIGHT = 2;
|
|
27
|
+
export const MODEL_RANKING_DEFAULT_LATENCY_WEIGHT = 1;
|
|
28
|
+
export const MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT = 1;
|
|
29
|
+
export const MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT = 2;
|
|
30
|
+
export const MODEL_OBSERVATION_FRESH_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
6
31
|
export const MAINTENANCE_INTERVAL_MS = 15 * 60 * 1_000;
|
|
7
32
|
export const TELEMETRY_POLL_INTERVAL_MS = 60_000;
|
|
8
33
|
export const TELEMETRY_STALE_AFTER_MS = 120_000;
|
|
@@ -12,20 +37,28 @@ export const FOOTER_CONTEXT_ERROR_FRACTION = 0.9;
|
|
|
12
37
|
export const FOOTER_BAR_MIN_WIDTH = 4;
|
|
13
38
|
export const FOOTER_BAR_MAX_WIDTH = 8;
|
|
14
39
|
export const FOOTER_WIDE_TERMINAL_WIDTH = 100;
|
|
15
|
-
export const FOOTER_COMPACTION_RENDER_INTERVAL_MS =
|
|
16
|
-
|
|
40
|
+
export const FOOTER_COMPACTION_RENDER_INTERVAL_MS = 500;
|
|
41
|
+
/** Half-period of the compacting liveness indicator; equal to the render tick so it visibly alternates every repaint. */
|
|
42
|
+
export const FOOTER_COMPACTION_BLINK_HALF_PERIOD_MS = 500;
|
|
17
43
|
export const MILLISECONDS_PER_SECOND = 1_000;
|
|
18
44
|
export const MILLISECONDS_PER_MINUTE = 60 * MILLISECONDS_PER_SECOND;
|
|
19
45
|
export const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE;
|
|
20
46
|
export const PAPYRUS_CONTEXT_INJECTION_CHANNEL = "papyrus.context-injection.v1";
|
|
21
47
|
export const PAPYRUS_CONTEXT_INJECTION_SCHEMA = "papyrus.context-injection/v1";
|
|
48
|
+
/** Matches Papyrus's own constants by convention; Jittor does not depend on the Papyrus package. */
|
|
49
|
+
export const PAPYRUS_TASK_FOCUS_CHANNEL = "papyrus.task-focus.v1";
|
|
50
|
+
export const PAPYRUS_TASK_FOCUS_SCHEMA = "papyrus.task-focus/v1";
|
|
51
|
+
export const TASK_FOCUS_EVENT_MAX_AGE_MS = 5 * MILLISECONDS_PER_MINUTE;
|
|
52
|
+
export const TASK_FOCUS_ID_MAX_LENGTH = 200;
|
|
53
|
+
export const TASK_COST_QUERY_LIMIT = 10_000;
|
|
54
|
+
/** A `metrics prune --before` cutoff newer than this must pass `force: true`. Guards against accidentally wiping recent/live data with a too-recent cutoff (e.g. "now"), while still allowing routine cleanup of genuinely old rows without ceremony. */
|
|
55
|
+
export const PRUNE_MIN_AGE_MS = 24 * MILLISECONDS_PER_HOUR;
|
|
22
56
|
export const CONTEXT_OBSERVATION_MAX_CHARACTERS = 10_000_000;
|
|
23
57
|
export const CONTEXT_OBSERVATION_MAX_AGE_MS = 5 * MILLISECONDS_PER_MINUTE;
|
|
24
58
|
export const CONTEXT_ASSESSMENT_QUERY_LIMIT = 10_000;
|
|
25
59
|
export const CONTEXT_EVENT_DEDUP_LIMIT = 1_000;
|
|
26
60
|
export const CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS = 24 * MILLISECONDS_PER_HOUR;
|
|
27
61
|
export const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
|
|
28
|
-
export const LOOPBACK_HOST = "127.0.0.1";
|
|
29
62
|
export const JITTOR_STATE_DIRECTORY = "jittor";
|
|
30
63
|
export const JITTOR_EXTENSION_SETTINGS_FILENAME = "extension.json";
|
|
31
64
|
export const DATABASE_FILENAME = "jittor.db";
|
|
@@ -34,8 +67,29 @@ export const HANDLE_FILENAME = "daemon.json";
|
|
|
34
67
|
export const SYSTEMD_UNIT_NAME = "jittor.service";
|
|
35
68
|
export const USAGE_CHART_HEIGHT = 8;
|
|
36
69
|
export const USAGE_Y_AXIS_WIDTH = 7;
|
|
37
|
-
|
|
70
|
+
/**
|
|
71
|
+
* A single flat "most recent N rows" query lets one heavy provider/model monopolize the entire
|
|
72
|
+
* budget within the query window, silently starving every other series out of the chart no matter
|
|
73
|
+
* which time frame is selected (a real bug: a single long, heavy session can fill 10k rows within
|
|
74
|
+
* a few hours, hiding a whole other provider's usage from a day earlier even in the Monthly view).
|
|
75
|
+
* The usage/cost dashboard instead fetches per distinct scope (see distinctScopes), bounded by
|
|
76
|
+
* these two limits; the worst-case total row volume (40 * 250 = 10,000) matches the old flat cap,
|
|
77
|
+
* but is now fairly distributed across every active series instead of claimable by just one.
|
|
78
|
+
*/
|
|
79
|
+
export const USAGE_MAX_DISTINCT_SCOPES = 40;
|
|
80
|
+
export const USAGE_PER_SCOPE_QUERY_LIMIT = 250;
|
|
81
|
+
export const USAGE_RENDER_MAX_SERIES = 20;
|
|
82
|
+
export const HUMAN_STATUS_MAX_SOURCES = 20;
|
|
83
|
+
export const HUMAN_TEXT_FIELD_MAX_CHARACTERS = 160;
|
|
84
|
+
export const CLI_METRICS_HUMAN_MAX_ROWS = 50;
|
|
85
|
+
export const CLI_AVAILABLE_ROUTES_MAX = 200;
|
|
86
|
+
/** Bounded rolling window for the compaction duration estimator; older samples are never fetched. */
|
|
87
|
+
export const COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES = 20;
|
|
88
|
+
/** Below this many samples the estimate stays explicit cold-start uncertainty rather than a guess. */
|
|
89
|
+
export const COMPACTION_DURATION_ESTIMATE_MIN_SAMPLES = 3;
|
|
38
90
|
export const MAX_USAGE_BUCKETS = 120;
|
|
91
|
+
/** Defense-in-depth cap on the SQL-side usage aggregation result: (scopes x metrics x buckets) is already small by construction, but this bounds it explicitly rather than trusting that alone. */
|
|
92
|
+
export const USAGE_AGGREGATE_MAX_ROWS = 25_000;
|
|
39
93
|
export const MAX_DYNAMIC_ROUTES = 100;
|
|
40
94
|
export const CODEX_ERROR_MESSAGE_LIMIT = 160;
|
|
41
95
|
export const CODEX_RETRY_AFTER_MAX_MS = 5 * MILLISECONDS_PER_MINUTE;
|
|
@@ -44,3 +98,13 @@ export const CODEX_RECOVERY_MAX_DELAY_MS = CODEX_RETRY_AFTER_MAX_MS;
|
|
|
44
98
|
export const CODEX_RECOVERY_MAX_ATTEMPTS = 3;
|
|
45
99
|
export const CODEX_RECOVERY_ATTEMPT_WINDOW_MS = 10 * MILLISECONDS_PER_MINUTE;
|
|
46
100
|
export const CODEX_RECOVERY_JITTER_RATIO = 0.2;
|
|
101
|
+
export const GOOGLE_VERTEX_BUDGET_DISPLAY_NAME_MAX_CHARACTERS = 160;
|
|
102
|
+
/** Bounded per-poll pull size; a budget subscription realistically holds at most a few pending notifications. */
|
|
103
|
+
export const GOOGLE_VERTEX_BUDGET_MAX_MESSAGES_PER_PULL = 20;
|
|
104
|
+
/**
|
|
105
|
+
* Lower than Codex's header-derived 0.8: this is Google's own documented "estimated ... subject to
|
|
106
|
+
* change until your invoice is finalized" data, delivered at-least-once and possibly out of order,
|
|
107
|
+
* multiple times per day rather than on every response.
|
|
108
|
+
*/
|
|
109
|
+
export const GOOGLE_VERTEX_BUDGET_CONFIDENCE = 0.6;
|
|
110
|
+
export const GOOGLE_ADC_TOKEN_REFRESH_SKEW_MS = 60_000;
|
package/src/daemon.ts
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { startDaemon as startDaemonKit, type RunningDaemon } from "@danypops/daemon-kit/daemon";
|
|
2
|
+
import { MAINTENANCE_INTERVAL_MS, TELEMETRY_POLL_INTERVAL_MS } from "./constants.ts";
|
|
2
3
|
import { DEFAULT_POLICY, UNCONFIGURED_ROUTE } from "./config.ts";
|
|
3
4
|
import { SQLiteMetricStore } from "./adapters/sqlite-metric-store.ts";
|
|
5
|
+
import { MetricBenchmarkStore } from "./adapters/metric-benchmark-store.ts";
|
|
6
|
+
import { OpenRouterBenchmarkIndexSource } from "./adapters/openrouter-benchmark-index-source.ts";
|
|
7
|
+
import { OpenRouterBenchmarkSource } from "./adapters/openrouter-benchmark-source.ts";
|
|
4
8
|
import { openJittorDb } from "./db.ts";
|
|
9
|
+
import { BenchmarkCatalog } from "./domain/benchmark.ts";
|
|
10
|
+
import { EvidenceModelRanker } from "./domain/model-ranking-service.ts";
|
|
5
11
|
import { createApp, JittorService } from "./service.ts";
|
|
6
12
|
import { JittorRouter } from "./router.ts";
|
|
13
|
+
import type { BenchmarkSource } from "./ports/benchmark-source.ts";
|
|
7
14
|
import type { TelemetrySource } from "./ports/telemetry-source.ts";
|
|
8
|
-
import { CodexTelemetrySource, OpenRouterTelemetrySource } from "./providers/telemetry-sources.ts";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type JittorPaths,
|
|
15
|
-
} from "./state.ts";
|
|
15
|
+
import { CodexTelemetrySource, GoogleVertexBudgetTelemetrySource, OpenRouterTelemetrySource } from "./providers/telemetry-sources.ts";
|
|
16
|
+
import { createGoogleAdcTokenProvider } from "./providers/google-adc-auth.ts";
|
|
17
|
+
import { GOOGLE_PUBSUB_READONLY_SCOPE } from "./providers/google-vertex-budget.ts";
|
|
18
|
+
import type { GoogleVertexMetricSource } from "./providers/google-vertex-contracts.ts";
|
|
19
|
+
import { ensureAuthToken, resolveJittorPaths, type JittorPaths } from "./state.ts";
|
|
20
|
+
import { logEvent, logger } from "./log.ts";
|
|
16
21
|
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
export type { RunningDaemon } from "@danypops/daemon-kit/daemon";
|
|
23
|
+
|
|
24
|
+
export function reportMaintenanceFailure(event: string, error: unknown): void {
|
|
25
|
+
logEvent("error", event, { message: error instanceof Error ? error.message : String(error) });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function benchmarkSourcesFromEnvironment(env: Record<string, string | undefined> = process.env): BenchmarkSource[] {
|
|
29
|
+
if (env["JITTOR_OPENROUTER_BENCHMARKS"] !== "1") return [];
|
|
30
|
+
const sources: BenchmarkSource[] = [new OpenRouterBenchmarkSource()];
|
|
31
|
+
if (env["OPENROUTER_API_KEY"]) sources.push(new OpenRouterBenchmarkIndexSource(env["OPENROUTER_API_KEY"]));
|
|
32
|
+
return sources;
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
export function telemetrySourcesFromEnvironment(env: Record<string, string | undefined> = process.env): TelemetrySource[] {
|
|
@@ -26,9 +38,27 @@ export function telemetrySourcesFromEnvironment(env: Record<string, string | und
|
|
|
26
38
|
if (codexAuthFile) sources.push(new CodexTelemetrySource(codexAuthFile));
|
|
27
39
|
const openRouterKey = env["OPENROUTER_API_KEY"];
|
|
28
40
|
if (openRouterKey) sources.push(new OpenRouterTelemetrySource(openRouterKey));
|
|
41
|
+
// Opt-in only: the Pub/Sub subscription is one-time GCP console/CLI setup outside Jittor (see
|
|
42
|
+
// docs/PROVIDER_RESEARCH.md), so its absence must never attempt ADC discovery or a network call.
|
|
43
|
+
const vertexBudgetSubscription = env["JITTOR_GOOGLE_VERTEX_BUDGET_SUBSCRIPTION"];
|
|
44
|
+
if (vertexBudgetSubscription) {
|
|
45
|
+
const source = (env["JITTOR_GOOGLE_VERTEX_BUDGET_SOURCE"] ?? "google-vertex") as GoogleVertexMetricSource;
|
|
46
|
+
const tokenProvider = createGoogleAdcTokenProvider([GOOGLE_PUBSUB_READONLY_SCOPE]);
|
|
47
|
+
sources.push(new GoogleVertexBudgetTelemetrySource(vertexBudgetSubscription, tokenProvider, Date.now, fetch, source));
|
|
48
|
+
}
|
|
29
49
|
return sources;
|
|
30
50
|
}
|
|
31
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Composition root, now built on `@danypops/daemon-kit/daemon`'s `startDaemon` for binding,
|
|
54
|
+
* atomic handle write, maintenance-timer driving, and clean shutdown -- the skeleton that used to
|
|
55
|
+
* be hand-rolled here (and, byte-identically, in web-spider-daemon's and papyrus's daemon.ts; see
|
|
56
|
+
* daemon-kit's README). Each maintenance task still catches and classifies its own failure via
|
|
57
|
+
* `reportMaintenanceFailure` (preserving Jittor's specific `checkpoint_failed`/
|
|
58
|
+
* `benchmark_refresh_failed`/`telemetry_poll_failed` event taxonomy) rather than relying on
|
|
59
|
+
* daemon-kit's own generic "maintenance task failed: <name>" catch, which exists as a safety net
|
|
60
|
+
* for tasks that don't self-classify, not to replace a consumer's own richer classification.
|
|
61
|
+
*/
|
|
32
62
|
export function startDaemon(
|
|
33
63
|
paths: JittorPaths = resolveJittorPaths(),
|
|
34
64
|
env: Record<string, string | undefined> = process.env,
|
|
@@ -36,6 +66,10 @@ export function startDaemon(
|
|
|
36
66
|
const token = ensureAuthToken(paths);
|
|
37
67
|
const metrics = new SQLiteMetricStore(openJittorDb(paths.database));
|
|
38
68
|
const sources = telemetrySourcesFromEnvironment(env);
|
|
69
|
+
const benchmarkSources = benchmarkSourcesFromEnvironment(env);
|
|
70
|
+
const benchmarkStore = new MetricBenchmarkStore(metrics);
|
|
71
|
+
const benchmarks = new BenchmarkCatalog(benchmarkStore, benchmarkSources);
|
|
72
|
+
const modelRanker = new EvidenceModelRanker(benchmarkStore, metrics);
|
|
39
73
|
const router = new JittorRouter({
|
|
40
74
|
metrics,
|
|
41
75
|
sources,
|
|
@@ -43,37 +77,25 @@ export function startDaemon(
|
|
|
43
77
|
routes: [],
|
|
44
78
|
currentRoute: UNCONFIGURED_ROUTE,
|
|
45
79
|
});
|
|
46
|
-
const service = new JittorService(metrics, router);
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
const service = new JittorService(metrics, router, benchmarks, modelRanker);
|
|
81
|
+
|
|
82
|
+
const daemon = startDaemonKit({
|
|
83
|
+
daemonLabel: "Jittor",
|
|
84
|
+
handlePath: paths.handle,
|
|
85
|
+
logger,
|
|
86
|
+
buildApp: () => createApp({ service, token }),
|
|
87
|
+
maintenanceTasks: [
|
|
88
|
+
{ name: "checkpoint", intervalMs: MAINTENANCE_INTERVAL_MS, run: async () => { await service.execute("service.checkpoint", {}).catch((error) => reportMaintenanceFailure("checkpoint_failed", error)); } },
|
|
89
|
+
{ name: "benchmark-refresh", intervalMs: MAINTENANCE_INTERVAL_MS, run: async () => { await benchmarks.refresh().catch((error) => reportMaintenanceFailure("benchmark_refresh_failed", error)); } },
|
|
90
|
+
{ name: "telemetry-poll", intervalMs: TELEMETRY_POLL_INTERVAL_MS, run: async () => { await router.poll().catch((error) => reportMaintenanceFailure("telemetry_poll_failed", error)); } },
|
|
91
|
+
],
|
|
92
|
+
onShutdown: () => { service.close(); },
|
|
52
93
|
});
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
writeDaemonHandle(paths, { host: LOOPBACK_HOST, port, pid: process.pid });
|
|
60
|
-
const maintenance = setInterval(() => { void service.execute("service.checkpoint", {}); }, MAINTENANCE_INTERVAL_MS);
|
|
61
|
-
const poll = setInterval(() => { void router.poll(); }, TELEMETRY_POLL_INTERVAL_MS);
|
|
62
|
-
if (sources.length > 0) void router.poll();
|
|
63
|
-
let stopped = false;
|
|
64
|
-
return {
|
|
65
|
-
host: LOOPBACK_HOST,
|
|
66
|
-
port,
|
|
67
|
-
async stop(): Promise<void> {
|
|
68
|
-
if (stopped) return;
|
|
69
|
-
stopped = true;
|
|
70
|
-
clearInterval(maintenance);
|
|
71
|
-
clearInterval(poll);
|
|
72
|
-
await server.stop(true);
|
|
73
|
-
service.close();
|
|
74
|
-
removeDaemonHandle(paths);
|
|
75
|
-
},
|
|
76
|
-
};
|
|
94
|
+
|
|
95
|
+
if (sources.length > 0) router.poll().catch((error) => reportMaintenanceFailure("telemetry_poll_failed", error));
|
|
96
|
+
if (benchmarkSources.length > 0) benchmarks.refresh().catch((error) => reportMaintenanceFailure("benchmark_refresh_failed", error));
|
|
97
|
+
|
|
98
|
+
return daemon;
|
|
77
99
|
}
|
|
78
100
|
|
|
79
101
|
export function serveMain(): void {
|
package/src/db.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { SQLITE_BUSY_TIMEOUT_MS, SQLITE_SCHEMA_VERSION } from "./constants.ts";
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { openSqliteWithPragmas } from "@danypops/daemon-kit/storage";
|
|
3
|
+
import { SQLITE_BUSY_TIMEOUT_MS } from "./constants.ts";
|
|
5
4
|
|
|
6
5
|
const INITIAL_SCHEMA = `
|
|
7
6
|
CREATE TABLE metric_observations (
|
|
@@ -20,31 +19,15 @@ CREATE INDEX metric_observations_time_idx
|
|
|
20
19
|
ON metric_observations(observed_at);
|
|
21
20
|
`;
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (row.user_version < 1) {
|
|
29
|
-
const migration = db.transaction(() => {
|
|
30
|
-
db.exec(INITIAL_SCHEMA);
|
|
31
|
-
db.exec("PRAGMA user_version = 1");
|
|
32
|
-
});
|
|
33
|
-
migration.immediate();
|
|
34
|
-
}
|
|
35
|
-
const migrated = db.query("PRAGMA user_version").get() as { user_version: number };
|
|
36
|
-
if (migrated.user_version !== SQLITE_SCHEMA_VERSION) {
|
|
37
|
-
throw new Error(`missing migration from schema ${migrated.user_version} to ${SQLITE_SCHEMA_VERSION}`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Delegates bootstrap (pragmas, migration engine) to `@danypops/daemon-kit/storage`, which
|
|
24
|
+
* generalizes the byte-identical pragma/PRAGMA-user_version skeleton jittor's own db.ts used to
|
|
25
|
+
* hand-roll (see daemon-kit's README). Jittor's only remaining responsibility is its own schema.
|
|
26
|
+
*/
|
|
41
27
|
export function openJittorDb(path: string): Database {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
migrate(db);
|
|
48
|
-
db.exec("PRAGMA optimize=0x10002");
|
|
49
|
-
return db;
|
|
28
|
+
return openSqliteWithPragmas(path, {
|
|
29
|
+
databaseOptions: { create: true, strict: true },
|
|
30
|
+
busyTimeoutMs: SQLITE_BUSY_TIMEOUT_MS,
|
|
31
|
+
migrations: [{ version: 1, up: (db) => db.exec(INITIAL_SCHEMA) }],
|
|
32
|
+
});
|
|
50
33
|
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BENCHMARK_DEFAULT_QUERY_LIMIT,
|
|
3
|
+
BENCHMARK_IDENTITY_MAX_CHARACTERS,
|
|
4
|
+
BENCHMARK_MAX_OBSERVATIONS_PER_SNAPSHOT,
|
|
5
|
+
BENCHMARK_MAX_QUERY_LIMIT,
|
|
6
|
+
BENCHMARK_MAX_TEXT_CHARACTERS,
|
|
7
|
+
BENCHMARK_REFRESH_INTERVAL_MS,
|
|
8
|
+
} from "../constants.ts";
|
|
9
|
+
import { METRIC_UNITS, type MetricUnit } from "./metric.ts";
|
|
10
|
+
import type { BenchmarkController } from "../ports/benchmark-controller.ts";
|
|
11
|
+
import type { BenchmarkSource as BenchmarkSourcePort } from "../ports/benchmark-source.ts";
|
|
12
|
+
import type { BenchmarkStore } from "../ports/benchmark-store.ts";
|
|
13
|
+
|
|
14
|
+
export type BenchmarkSourceType = "creator" | "marketplace" | "independent" | "operational" | "preference" | "local";
|
|
15
|
+
|
|
16
|
+
export interface ModelIdentity {
|
|
17
|
+
provider: string;
|
|
18
|
+
model: string;
|
|
19
|
+
version: string | null;
|
|
20
|
+
canonical: string;
|
|
21
|
+
aliases: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BenchmarkProvenance {
|
|
25
|
+
sourceId: string;
|
|
26
|
+
sourceType: BenchmarkSourceType;
|
|
27
|
+
publisher: string;
|
|
28
|
+
url: string;
|
|
29
|
+
revision: string;
|
|
30
|
+
publishedAt: number | null;
|
|
31
|
+
retrievedAt: number;
|
|
32
|
+
freshUntil: number;
|
|
33
|
+
license: string;
|
|
34
|
+
confidence: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BenchmarkObservation {
|
|
38
|
+
model: ModelIdentity;
|
|
39
|
+
dimension: string;
|
|
40
|
+
value: number;
|
|
41
|
+
unit: MetricUnit;
|
|
42
|
+
provenance: BenchmarkProvenance;
|
|
43
|
+
methodology: Record<string, string | number | boolean | null | string[]>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BenchmarkSourceSnapshot {
|
|
47
|
+
sourceId: string;
|
|
48
|
+
snapshotId: string;
|
|
49
|
+
retrievedAt: number;
|
|
50
|
+
observations: BenchmarkObservation[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BenchmarkSnapshot extends BenchmarkSourceSnapshot {
|
|
54
|
+
publishedAt: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface BenchmarkSourceStatus {
|
|
58
|
+
id: string;
|
|
59
|
+
ok: boolean | null;
|
|
60
|
+
hasEvidence: boolean;
|
|
61
|
+
lastAttemptAt: number | null;
|
|
62
|
+
lastSuccessAt: number | null;
|
|
63
|
+
observations: number;
|
|
64
|
+
error?: "source refresh failed";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface BenchmarkRefreshResult {
|
|
68
|
+
observedAt: number;
|
|
69
|
+
sources: BenchmarkSourceStatus[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface BenchmarkQuery {
|
|
73
|
+
sourceId: string;
|
|
74
|
+
model?: string;
|
|
75
|
+
dimension?: string;
|
|
76
|
+
limit?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface BenchmarkQueryResult extends BenchmarkSnapshot {
|
|
80
|
+
completeness: "complete" | "truncated";
|
|
81
|
+
freshness: "fresh" | "stale";
|
|
82
|
+
freshUntil: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface BenchmarkCatalogOptions {
|
|
86
|
+
clock?: () => number;
|
|
87
|
+
refreshIntervalMs?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type BenchmarkSource = BenchmarkSourcePort;
|
|
91
|
+
|
|
92
|
+
const VERSION_SUFFIX = /-(\d{4}-\d{2}-\d{2})$/;
|
|
93
|
+
const SOURCE_TYPES = new Set<BenchmarkSourceType>(["creator", "marketplace", "independent", "operational", "preference", "local"]);
|
|
94
|
+
|
|
95
|
+
function boundedText(value: unknown, name: string, maximum = BENCHMARK_MAX_TEXT_CHARACTERS): string {
|
|
96
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${name} is required`);
|
|
97
|
+
const normalized = value.trim();
|
|
98
|
+
if (normalized.length > maximum) throw new Error(`${name} exceeds the length limit`);
|
|
99
|
+
if (/\p{Cc}/u.test(normalized)) throw new Error(`${name} contains control characters`);
|
|
100
|
+
return normalized;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function identityPart(value: string, name: string, allowPath = false): string {
|
|
104
|
+
const normalized = boundedText(value, name, BENCHMARK_IDENTITY_MAX_CHARACTERS).toLowerCase();
|
|
105
|
+
const pattern = allowPath ? /^[a-z0-9][a-z0-9._:+/-]*$/ : /^[a-z0-9][a-z0-9._:+-]*$/;
|
|
106
|
+
if (!pattern.test(normalized) || normalized.includes("//")) throw new Error(`${name} is invalid`);
|
|
107
|
+
return normalized;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function normalizeModelIdentity(provider: string, model: string, aliases: string[] = []): ModelIdentity {
|
|
111
|
+
const normalizedProvider = identityPart(provider, "provider");
|
|
112
|
+
const normalizedModel = identityPart(model, "model", true);
|
|
113
|
+
const version = VERSION_SUFFIX.exec(normalizedModel)?.[1] ?? null;
|
|
114
|
+
const canonical = `${normalizedProvider}/${normalizedModel}`;
|
|
115
|
+
const normalizedAliases = [...new Set(aliases.map((alias) => boundedText(alias, "alias", BENCHMARK_IDENTITY_MAX_CHARACTERS).toLowerCase()))]
|
|
116
|
+
.filter((alias) => alias !== canonical)
|
|
117
|
+
.sort();
|
|
118
|
+
return { provider: normalizedProvider, model: normalizedModel, version, canonical, aliases: normalizedAliases };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateIdentity(value: unknown): ModelIdentity {
|
|
122
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("model identity is required");
|
|
123
|
+
const input = value as Record<string, unknown>;
|
|
124
|
+
if (!Array.isArray(input["aliases"]) || !input["aliases"].every((alias) => typeof alias === "string")) throw new Error("model aliases are invalid");
|
|
125
|
+
const normalized = normalizeModelIdentity(String(input["provider"] ?? ""), String(input["model"] ?? ""), input["aliases"] as string[]);
|
|
126
|
+
if (input["canonical"] !== normalized.canonical || input["version"] !== normalized.version) throw new Error("model identity is not normalized");
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateTimestamp(value: unknown, name: string, nullable = false): number | null {
|
|
131
|
+
if (nullable && value === null) return null;
|
|
132
|
+
if (!Number.isSafeInteger(value) || (value as number) <= 0) throw new Error(`${name} must be a positive integer timestamp`);
|
|
133
|
+
return value as number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateProvenance(value: unknown): BenchmarkProvenance {
|
|
137
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("provenance is required");
|
|
138
|
+
const input = value as Record<string, unknown>;
|
|
139
|
+
const sourceId = identityPart(String(input["sourceId"] ?? ""), "source id");
|
|
140
|
+
if (!SOURCE_TYPES.has(input["sourceType"] as BenchmarkSourceType)) throw new Error("source type is invalid");
|
|
141
|
+
let url: URL;
|
|
142
|
+
try { url = new URL(boundedText(input["url"], "source URL")); } catch { throw new Error("source URL is invalid"); }
|
|
143
|
+
if (url.protocol !== "https:") throw new Error("source URL must use HTTPS");
|
|
144
|
+
const confidence = input["confidence"];
|
|
145
|
+
if (typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) throw new Error("confidence must be between zero and one");
|
|
146
|
+
const retrievedAt = validateTimestamp(input["retrievedAt"], "retrieval time") as number;
|
|
147
|
+
const freshUntil = validateTimestamp(input["freshUntil"], "freshness deadline") as number;
|
|
148
|
+
if (freshUntil < retrievedAt) throw new Error("freshness deadline precedes retrieval");
|
|
149
|
+
return {
|
|
150
|
+
sourceId,
|
|
151
|
+
sourceType: input["sourceType"] as BenchmarkSourceType,
|
|
152
|
+
publisher: boundedText(input["publisher"], "publisher"),
|
|
153
|
+
url: url.toString(),
|
|
154
|
+
revision: boundedText(input["revision"], "revision"),
|
|
155
|
+
publishedAt: validateTimestamp(input["publishedAt"], "publication time", true),
|
|
156
|
+
retrievedAt,
|
|
157
|
+
freshUntil,
|
|
158
|
+
license: boundedText(input["license"], "license"),
|
|
159
|
+
confidence,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validateMethodology(value: unknown): BenchmarkObservation["methodology"] {
|
|
164
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("methodology is required");
|
|
165
|
+
const serialized = JSON.stringify(value);
|
|
166
|
+
if (serialized.length > BENCHMARK_MAX_TEXT_CHARACTERS) throw new Error("methodology exceeds the size limit");
|
|
167
|
+
return structuredClone(value as BenchmarkObservation["methodology"]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function validateBenchmarkObservation(value: unknown): BenchmarkObservation {
|
|
171
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("benchmark observation must be an object");
|
|
172
|
+
const input = value as Record<string, unknown>;
|
|
173
|
+
const numericValue = input["value"];
|
|
174
|
+
if (typeof numericValue !== "number" || !Number.isFinite(numericValue)) throw new Error("benchmark value must be finite");
|
|
175
|
+
if (!METRIC_UNITS.includes(input["unit"] as MetricUnit)) throw new Error("benchmark unit is not supported");
|
|
176
|
+
return {
|
|
177
|
+
model: validateIdentity(input["model"]),
|
|
178
|
+
dimension: identityPart(String(input["dimension"] ?? ""), "dimension"),
|
|
179
|
+
value: numericValue,
|
|
180
|
+
unit: input["unit"] as MetricUnit,
|
|
181
|
+
provenance: validateProvenance(input["provenance"]),
|
|
182
|
+
methodology: validateMethodology(input["methodology"]),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateSourceSnapshot(value: BenchmarkSourceSnapshot, expectedSourceId: string): BenchmarkSourceSnapshot {
|
|
187
|
+
const sourceId = identityPart(value.sourceId, "source id");
|
|
188
|
+
if (sourceId !== expectedSourceId) throw new Error("source snapshot identity mismatch");
|
|
189
|
+
const snapshotId = boundedText(value.snapshotId, "snapshot id", BENCHMARK_IDENTITY_MAX_CHARACTERS);
|
|
190
|
+
const retrievedAt = validateTimestamp(value.retrievedAt, "retrieval time") as number;
|
|
191
|
+
if (!Array.isArray(value.observations) || value.observations.length > BENCHMARK_MAX_OBSERVATIONS_PER_SNAPSHOT) throw new Error("source snapshot exceeds the observation limit");
|
|
192
|
+
const observations = value.observations.map(validateBenchmarkObservation);
|
|
193
|
+
if (observations.some((observation) => observation.provenance.sourceId !== sourceId || observation.provenance.retrievedAt !== retrievedAt)) {
|
|
194
|
+
throw new Error("source snapshot provenance mismatch");
|
|
195
|
+
}
|
|
196
|
+
return { sourceId, snapshotId, retrievedAt, observations };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export class BenchmarkCatalog implements BenchmarkController {
|
|
200
|
+
private readonly clock: () => number;
|
|
201
|
+
private readonly refreshIntervalMs: number;
|
|
202
|
+
private readonly states = new Map<string, BenchmarkSourceStatus>();
|
|
203
|
+
|
|
204
|
+
constructor(
|
|
205
|
+
private readonly store: BenchmarkStore,
|
|
206
|
+
private readonly sources: BenchmarkSource[],
|
|
207
|
+
options: BenchmarkCatalogOptions = {},
|
|
208
|
+
) {
|
|
209
|
+
this.clock = options.clock ?? Date.now;
|
|
210
|
+
this.refreshIntervalMs = options.refreshIntervalMs ?? BENCHMARK_REFRESH_INTERVAL_MS;
|
|
211
|
+
for (const source of sources) {
|
|
212
|
+
const evidence = store.latest(source.id);
|
|
213
|
+
this.states.set(source.id, {
|
|
214
|
+
id: source.id,
|
|
215
|
+
ok: null,
|
|
216
|
+
hasEvidence: evidence !== null,
|
|
217
|
+
lastAttemptAt: null,
|
|
218
|
+
lastSuccessAt: evidence?.retrievedAt ?? null,
|
|
219
|
+
observations: evidence?.observations.length ?? 0,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async refresh(force = false): Promise<BenchmarkRefreshResult> {
|
|
225
|
+
const now = this.clock();
|
|
226
|
+
await Promise.all(this.sources.map(async (source) => {
|
|
227
|
+
const prior = this.states.get(source.id)!;
|
|
228
|
+
if (!force && prior.lastAttemptAt !== null && now - prior.lastAttemptAt < this.refreshIntervalMs) return;
|
|
229
|
+
this.states.set(source.id, { ...prior, lastAttemptAt: now });
|
|
230
|
+
try {
|
|
231
|
+
const snapshot = validateSourceSnapshot(await source.fetch(), source.id);
|
|
232
|
+
const published = this.store.publish(snapshot.sourceId, snapshot.snapshotId, snapshot.observations);
|
|
233
|
+
this.states.set(source.id, { id: source.id, ok: true, hasEvidence: true, lastAttemptAt: now, lastSuccessAt: published.retrievedAt, observations: published.observations.length });
|
|
234
|
+
} catch {
|
|
235
|
+
const evidence = this.store.latest(source.id);
|
|
236
|
+
this.states.set(source.id, { id: source.id, ok: false, hasEvidence: evidence !== null, lastAttemptAt: now, lastSuccessAt: evidence?.retrievedAt ?? prior.lastSuccessAt, observations: evidence?.observations.length ?? prior.observations, error: "source refresh failed" });
|
|
237
|
+
}
|
|
238
|
+
}));
|
|
239
|
+
return { observedAt: now, sources: this.status().sources };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
status(): BenchmarkRefreshResult {
|
|
243
|
+
return { observedAt: this.clock(), sources: this.sources.map((source) => structuredClone(this.states.get(source.id)!)) };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
query(input: BenchmarkQuery): BenchmarkQueryResult {
|
|
247
|
+
const sourceId = identityPart(input.sourceId, "source id");
|
|
248
|
+
const snapshot = this.store.latest(sourceId);
|
|
249
|
+
if (!snapshot) throw new Error("benchmark evidence is not available for the source");
|
|
250
|
+
const requestedLimit = Number.isFinite(input.limit) ? Math.floor(input.limit!) : BENCHMARK_DEFAULT_QUERY_LIMIT;
|
|
251
|
+
const limit = Math.max(1, Math.min(BENCHMARK_MAX_QUERY_LIMIT, requestedLimit));
|
|
252
|
+
const model = input.model?.trim().toLowerCase();
|
|
253
|
+
const dimension = input.dimension?.trim().toLowerCase();
|
|
254
|
+
const matched = snapshot.observations.filter((observation) => (!model || observation.model.canonical === model || observation.model.aliases.includes(model)) && (!dimension || observation.dimension === dimension));
|
|
255
|
+
const freshUntil = Math.min(...snapshot.observations.map((observation) => observation.provenance.freshUntil));
|
|
256
|
+
return {
|
|
257
|
+
...structuredClone(snapshot),
|
|
258
|
+
observations: structuredClone(matched.slice(0, limit)),
|
|
259
|
+
completeness: matched.length > limit ? "truncated" : "complete",
|
|
260
|
+
freshness: this.clock() <= freshUntil ? "fresh" : "stale",
|
|
261
|
+
freshUntil,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|