@danypops/jittor 0.14.0 → 0.16.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/package.json +4 -3
- package/src/adapters/artificial-analysis-direct-source.ts +42 -11
- package/src/adapters/lmarena-hf-source.ts +37 -15
- package/src/adapters/metric-benchmark-store.ts +29 -18
- package/src/adapters/openrouter-benchmark-source.ts +75 -19
- package/src/adapters/openrouter-design-arena-source.ts +36 -19
- package/src/adapters/sqlite-metric-store.ts +48 -25
- package/src/adapters/sqlite-session-identity-store.ts +13 -7
- package/src/cli-commands/benchmarks.ts +87 -22
- package/src/cli-commands/compaction.ts +7 -2
- package/src/cli-commands/context.ts +10 -2
- package/src/cli-commands/metrics.ts +128 -31
- package/src/cli-commands/op.ts +6 -1
- package/src/cli-commands/route-args.ts +5 -1
- package/src/cli-commands/router.ts +61 -18
- package/src/cli-commands/service-daemon.ts +31 -13
- package/src/cli-commands/session.ts +15 -4
- package/src/cli-commands/support.ts +1 -1
- package/src/cli.ts +30 -23
- package/src/client.ts +1 -1
- package/src/constants.ts +1 -1
- package/src/daemon.ts +44 -24
- package/src/domain/benchmark.ts +72 -40
- package/src/domain/codex-recovery.ts +34 -25
- package/src/domain/context-hub.ts +35 -26
- package/src/domain/context-telemetry.ts +106 -35
- package/src/domain/metric.ts +11 -11
- package/src/domain/model-observation.ts +139 -55
- package/src/domain/model-ranking-service.ts +13 -3
- package/src/domain/model-ranking.ts +126 -60
- package/src/domain/task-cost.ts +52 -11
- package/src/domain/task-focus.ts +11 -8
- package/src/domain/usage.ts +2 -2
- package/src/index.ts +69 -69
- package/src/log.ts +7 -2
- package/src/operations/benchmark-operations.ts +1 -1
- package/src/operations/context-operations.ts +15 -6
- package/src/operations/metrics-operations.ts +63 -28
- package/src/operations/model-ranking-operations.ts +9 -2
- package/src/operations/router-operations.ts +8 -4
- package/src/operations/session-identity-operations.ts +1 -1
- package/src/operations/session-scope.ts +5 -3
- package/src/policy.ts +22 -17
- package/src/ports/benchmark-controller.ts +1 -5
- package/src/ports/metric-store.ts +1 -1
- package/src/providers/anthropic-contracts.ts +13 -3
- package/src/providers/codex-contracts.ts +60 -52
- package/src/providers/codex.ts +16 -19
- package/src/providers/google-vertex-budget-contracts.ts +24 -14
- package/src/providers/google-vertex-budget.ts +15 -13
- package/src/providers/google-vertex-contracts.ts +36 -24
- package/src/providers/openrouter-contracts.ts +49 -51
- package/src/providers/openrouter.ts +21 -15
- package/src/providers/telemetry-sources.ts +13 -10
- package/src/router.ts +92 -43
- package/src/service.ts +93 -32
- package/src/session-identity-service.ts +10 -2
- package/src/state.ts +4 -10
- package/src/vehicle-registration.ts +154 -0
package/src/domain/benchmark.ts
CHANGED
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
BENCHMARK_MAX_TEXT_CHARACTERS,
|
|
7
7
|
BENCHMARK_REFRESH_INTERVAL_MS,
|
|
8
8
|
} from "../constants.ts";
|
|
9
|
-
import { METRIC_UNITS, type MetricUnit } from "./metric.ts";
|
|
10
9
|
import type { BenchmarkController } from "../ports/benchmark-controller.ts";
|
|
11
10
|
import type { BenchmarkSource as BenchmarkSourcePort } from "../ports/benchmark-source.ts";
|
|
12
11
|
import type { BenchmarkStore } from "../ports/benchmark-store.ts";
|
|
12
|
+
import { METRIC_UNITS, type MetricUnit } from "./metric.ts";
|
|
13
13
|
|
|
14
14
|
export type BenchmarkSourceType = "creator" | "marketplace" | "independent" | "operational" | "preference" | "local";
|
|
15
15
|
|
|
@@ -112,7 +112,9 @@ export function normalizeModelIdentity(provider: string, model: string, aliases:
|
|
|
112
112
|
const normalizedModel = identityPart(model, "model", true);
|
|
113
113
|
const version = VERSION_SUFFIX.exec(normalizedModel)?.[1] ?? null;
|
|
114
114
|
const canonical = `${normalizedProvider}/${normalizedModel}`;
|
|
115
|
-
const normalizedAliases = [
|
|
115
|
+
const normalizedAliases = [
|
|
116
|
+
...new Set(aliases.map((alias) => boundedText(alias, "alias", BENCHMARK_IDENTITY_MAX_CHARACTERS).toLowerCase())),
|
|
117
|
+
]
|
|
116
118
|
.filter((alias) => alias !== canonical)
|
|
117
119
|
.sort();
|
|
118
120
|
return { provider: normalizedProvider, model: normalizedModel, version, canonical, aliases: normalizedAliases };
|
|
@@ -121,9 +123,10 @@ export function normalizeModelIdentity(provider: string, model: string, aliases:
|
|
|
121
123
|
function validateIdentity(value: unknown): ModelIdentity {
|
|
122
124
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("model identity is required");
|
|
123
125
|
const input = value as Record<string, unknown>;
|
|
124
|
-
if (!Array.isArray(input
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
if (!Array.isArray(input.aliases) || !input.aliases.every((alias) => typeof alias === "string"))
|
|
127
|
+
throw new Error("model aliases are invalid");
|
|
128
|
+
const normalized = normalizeModelIdentity(String(input.provider ?? ""), String(input.model ?? ""), input.aliases as string[]);
|
|
129
|
+
if (input.canonical !== normalized.canonical || input.version !== normalized.version) throw new Error("model identity is not normalized");
|
|
127
130
|
return normalized;
|
|
128
131
|
}
|
|
129
132
|
|
|
@@ -136,26 +139,31 @@ function validateTimestamp(value: unknown, name: string, nullable = false): numb
|
|
|
136
139
|
function validateProvenance(value: unknown): BenchmarkProvenance {
|
|
137
140
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("provenance is required");
|
|
138
141
|
const input = value as Record<string, unknown>;
|
|
139
|
-
const sourceId = identityPart(String(input
|
|
140
|
-
if (!SOURCE_TYPES.has(input
|
|
142
|
+
const sourceId = identityPart(String(input.sourceId ?? ""), "source id");
|
|
143
|
+
if (!SOURCE_TYPES.has(input.sourceType as BenchmarkSourceType)) throw new Error("source type is invalid");
|
|
141
144
|
let url: URL;
|
|
142
|
-
try {
|
|
145
|
+
try {
|
|
146
|
+
url = new URL(boundedText(input.url, "source URL"));
|
|
147
|
+
} catch {
|
|
148
|
+
throw new Error("source URL is invalid");
|
|
149
|
+
}
|
|
143
150
|
if (url.protocol !== "https:") throw new Error("source URL must use HTTPS");
|
|
144
|
-
const confidence = input
|
|
145
|
-
if (typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1)
|
|
146
|
-
|
|
147
|
-
const
|
|
151
|
+
const confidence = input.confidence;
|
|
152
|
+
if (typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1)
|
|
153
|
+
throw new Error("confidence must be between zero and one");
|
|
154
|
+
const retrievedAt = validateTimestamp(input.retrievedAt, "retrieval time") as number;
|
|
155
|
+
const freshUntil = validateTimestamp(input.freshUntil, "freshness deadline") as number;
|
|
148
156
|
if (freshUntil < retrievedAt) throw new Error("freshness deadline precedes retrieval");
|
|
149
157
|
return {
|
|
150
158
|
sourceId,
|
|
151
|
-
sourceType: input
|
|
152
|
-
publisher: boundedText(input
|
|
159
|
+
sourceType: input.sourceType as BenchmarkSourceType,
|
|
160
|
+
publisher: boundedText(input.publisher, "publisher"),
|
|
153
161
|
url: url.toString(),
|
|
154
|
-
revision: boundedText(input
|
|
155
|
-
publishedAt: validateTimestamp(input
|
|
162
|
+
revision: boundedText(input.revision, "revision"),
|
|
163
|
+
publishedAt: validateTimestamp(input.publishedAt, "publication time", true),
|
|
156
164
|
retrievedAt,
|
|
157
165
|
freshUntil,
|
|
158
|
-
license: boundedText(input
|
|
166
|
+
license: boundedText(input.license, "license"),
|
|
159
167
|
confidence,
|
|
160
168
|
};
|
|
161
169
|
}
|
|
@@ -170,16 +178,16 @@ function validateMethodology(value: unknown): BenchmarkObservation["methodology"
|
|
|
170
178
|
export function validateBenchmarkObservation(value: unknown): BenchmarkObservation {
|
|
171
179
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("benchmark observation must be an object");
|
|
172
180
|
const input = value as Record<string, unknown>;
|
|
173
|
-
const numericValue = input
|
|
181
|
+
const numericValue = input.value;
|
|
174
182
|
if (typeof numericValue !== "number" || !Number.isFinite(numericValue)) throw new Error("benchmark value must be finite");
|
|
175
|
-
if (!METRIC_UNITS.includes(input
|
|
183
|
+
if (!METRIC_UNITS.includes(input.unit as MetricUnit)) throw new Error("benchmark unit is not supported");
|
|
176
184
|
return {
|
|
177
|
-
model: validateIdentity(input
|
|
178
|
-
dimension: identityPart(String(input
|
|
185
|
+
model: validateIdentity(input.model),
|
|
186
|
+
dimension: identityPart(String(input.dimension ?? ""), "dimension"),
|
|
179
187
|
value: numericValue,
|
|
180
|
-
unit: input
|
|
181
|
-
provenance: validateProvenance(input
|
|
182
|
-
methodology: validateMethodology(input
|
|
188
|
+
unit: input.unit as MetricUnit,
|
|
189
|
+
provenance: validateProvenance(input.provenance),
|
|
190
|
+
methodology: validateMethodology(input.methodology),
|
|
183
191
|
};
|
|
184
192
|
}
|
|
185
193
|
|
|
@@ -188,9 +196,12 @@ function validateSourceSnapshot(value: BenchmarkSourceSnapshot, expectedSourceId
|
|
|
188
196
|
if (sourceId !== expectedSourceId) throw new Error("source snapshot identity mismatch");
|
|
189
197
|
const snapshotId = boundedText(value.snapshotId, "snapshot id", BENCHMARK_IDENTITY_MAX_CHARACTERS);
|
|
190
198
|
const retrievedAt = validateTimestamp(value.retrievedAt, "retrieval time") as number;
|
|
191
|
-
if (!Array.isArray(value.observations) || value.observations.length > BENCHMARK_MAX_OBSERVATIONS_PER_SNAPSHOT)
|
|
199
|
+
if (!Array.isArray(value.observations) || value.observations.length > BENCHMARK_MAX_OBSERVATIONS_PER_SNAPSHOT)
|
|
200
|
+
throw new Error("source snapshot exceeds the observation limit");
|
|
192
201
|
const observations = value.observations.map(validateBenchmarkObservation);
|
|
193
|
-
if (
|
|
202
|
+
if (
|
|
203
|
+
observations.some((observation) => observation.provenance.sourceId !== sourceId || observation.provenance.retrievedAt !== retrievedAt)
|
|
204
|
+
) {
|
|
194
205
|
throw new Error("source snapshot provenance mismatch");
|
|
195
206
|
}
|
|
196
207
|
return { sourceId, snapshotId, retrievedAt, observations };
|
|
@@ -223,19 +234,36 @@ export class BenchmarkCatalog implements BenchmarkController {
|
|
|
223
234
|
|
|
224
235
|
async refresh(force = false): Promise<BenchmarkRefreshResult> {
|
|
225
236
|
const now = this.clock();
|
|
226
|
-
await Promise.all(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
await Promise.all(
|
|
238
|
+
this.sources.map(async (source) => {
|
|
239
|
+
const prior = this.states.get(source.id)!;
|
|
240
|
+
if (!force && prior.lastAttemptAt !== null && now - prior.lastAttemptAt < this.refreshIntervalMs) return;
|
|
241
|
+
this.states.set(source.id, { ...prior, lastAttemptAt: now });
|
|
242
|
+
try {
|
|
243
|
+
const snapshot = validateSourceSnapshot(await source.fetch(), source.id);
|
|
244
|
+
const published = this.store.publish(snapshot.sourceId, snapshot.snapshotId, snapshot.observations);
|
|
245
|
+
this.states.set(source.id, {
|
|
246
|
+
id: source.id,
|
|
247
|
+
ok: true,
|
|
248
|
+
hasEvidence: true,
|
|
249
|
+
lastAttemptAt: now,
|
|
250
|
+
lastSuccessAt: published.retrievedAt,
|
|
251
|
+
observations: published.observations.length,
|
|
252
|
+
});
|
|
253
|
+
} catch {
|
|
254
|
+
const evidence = this.store.latest(source.id);
|
|
255
|
+
this.states.set(source.id, {
|
|
256
|
+
id: source.id,
|
|
257
|
+
ok: false,
|
|
258
|
+
hasEvidence: evidence !== null,
|
|
259
|
+
lastAttemptAt: now,
|
|
260
|
+
lastSuccessAt: evidence?.retrievedAt ?? prior.lastSuccessAt,
|
|
261
|
+
observations: evidence?.observations.length ?? prior.observations,
|
|
262
|
+
error: "source refresh failed",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
239
267
|
return { observedAt: now, sources: this.status().sources };
|
|
240
268
|
}
|
|
241
269
|
|
|
@@ -251,7 +279,11 @@ export class BenchmarkCatalog implements BenchmarkController {
|
|
|
251
279
|
const limit = Math.max(1, Math.min(BENCHMARK_MAX_QUERY_LIMIT, requestedLimit));
|
|
252
280
|
const model = input.model?.trim().toLowerCase();
|
|
253
281
|
const dimension = input.dimension?.trim().toLowerCase();
|
|
254
|
-
const matched = snapshot.observations.filter(
|
|
282
|
+
const matched = snapshot.observations.filter(
|
|
283
|
+
(observation) =>
|
|
284
|
+
(!model || observation.model.canonical === model || observation.model.aliases.includes(model)) &&
|
|
285
|
+
(!dimension || observation.dimension === dimension),
|
|
286
|
+
);
|
|
255
287
|
const freshUntil = Math.min(...snapshot.observations.map((observation) => observation.provenance.freshUntil));
|
|
256
288
|
return {
|
|
257
289
|
...structuredClone(snapshot),
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CODEX_ERROR_MESSAGE_LIMIT,
|
|
3
|
-
CODEX_RETRY_AFTER_MAX_MS,
|
|
4
|
-
MILLISECONDS_PER_SECOND,
|
|
5
|
-
} from "../constants.ts";
|
|
1
|
+
import { CODEX_ERROR_MESSAGE_LIMIT, CODEX_RETRY_AFTER_MAX_MS, MILLISECONDS_PER_SECOND } from "../constants.ts";
|
|
6
2
|
|
|
7
3
|
export type CodexFailureKind =
|
|
8
4
|
| "concurrency"
|
|
@@ -57,10 +53,12 @@ export class CodexRecoveryPolicy {
|
|
|
57
53
|
private readonly random: () => number = Math.random,
|
|
58
54
|
) {
|
|
59
55
|
if (!Number.isFinite(options.baseDelayMs) || options.baseDelayMs < 0) throw new Error("baseDelayMs must be non-negative");
|
|
60
|
-
if (!Number.isFinite(options.maxDelayMs) || options.maxDelayMs < options.baseDelayMs)
|
|
56
|
+
if (!Number.isFinite(options.maxDelayMs) || options.maxDelayMs < options.baseDelayMs)
|
|
57
|
+
throw new Error("maxDelayMs must be at least baseDelayMs");
|
|
61
58
|
if (!Number.isInteger(options.maxAttempts) || options.maxAttempts < 1) throw new Error("maxAttempts must be a positive integer");
|
|
62
59
|
if (!Number.isFinite(options.attemptWindowMs) || options.attemptWindowMs <= 0) throw new Error("attemptWindowMs must be positive");
|
|
63
|
-
if (!Number.isFinite(options.jitterRatio) || options.jitterRatio < 0 || options.jitterRatio > 1)
|
|
60
|
+
if (!Number.isFinite(options.jitterRatio) || options.jitterRatio < 0 || options.jitterRatio > 1)
|
|
61
|
+
throw new Error("jitterRatio must be between 0 and 1");
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
observeFailure(failure: CodexFailure, now: number): void {
|
|
@@ -88,13 +86,15 @@ export class CodexRecoveryPolicy {
|
|
|
88
86
|
this.normalizeWindow(now);
|
|
89
87
|
if (!this.pendingFailure) return { action: "wait", reason: "no transient Codex failure is pending" };
|
|
90
88
|
if (this.attempts >= this.options.maxAttempts) {
|
|
91
|
-
return {
|
|
89
|
+
return {
|
|
90
|
+
action: "exhausted",
|
|
91
|
+
reason: `${this.options.maxAttempts} recovery attempts reached within ${this.options.attemptWindowMs}ms`,
|
|
92
|
+
};
|
|
92
93
|
}
|
|
93
|
-
const base = this.pendingFailure.retryAfterMs
|
|
94
|
-
?? this.options.baseDelayMs * (2 ** this.attempts);
|
|
94
|
+
const base = this.pendingFailure.retryAfterMs ?? this.options.baseDelayMs * 2 ** this.attempts;
|
|
95
95
|
const sample = this.random();
|
|
96
96
|
const unit = Number.isFinite(sample) ? Math.min(1, Math.max(0, sample)) : 0;
|
|
97
|
-
const multiplier = 1 + (
|
|
97
|
+
const multiplier = 1 + (unit * 2 - 1) * this.options.jitterRatio;
|
|
98
98
|
const jittered = Math.max(0, Math.round(base * multiplier));
|
|
99
99
|
const delayMs = this.pendingFailure.retryAfterMs === undefined ? jittered : Math.max(base, jittered);
|
|
100
100
|
return {
|
|
@@ -135,9 +135,7 @@ export class CodexRecoveryPolicy {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
138
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
139
|
-
? value as Record<string, unknown>
|
|
140
|
-
: undefined;
|
|
138
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
141
139
|
}
|
|
142
140
|
|
|
143
141
|
function firstString(...values: unknown[]): string | undefined {
|
|
@@ -160,15 +158,15 @@ function matches(value: string, patterns: readonly string[]): boolean {
|
|
|
160
158
|
|
|
161
159
|
export function classifyCodexFailure(value: unknown, metadata: CodexFailureMetadata = {}): CodexFailure {
|
|
162
160
|
const root = asRecord(value);
|
|
163
|
-
const detail = asRecord(root?.
|
|
164
|
-
const nestedError = asRecord(root?.
|
|
165
|
-
const code = firstString(detail?.
|
|
166
|
-
const source = firstString(detail?.
|
|
161
|
+
const detail = asRecord(root?.detail);
|
|
162
|
+
const nestedError = asRecord(root?.error);
|
|
163
|
+
const code = firstString(detail?.code, detail?.error_code, nestedError?.code, root?.code);
|
|
164
|
+
const source = firstString(detail?.source, nestedError?.source, root?.source);
|
|
167
165
|
const rawMessage = firstString(
|
|
168
|
-
detail?.
|
|
169
|
-
typeof root?.
|
|
170
|
-
nestedError?.
|
|
171
|
-
root?.
|
|
166
|
+
detail?.message,
|
|
167
|
+
typeof root?.error === "string" ? root.error : undefined,
|
|
168
|
+
nestedError?.message,
|
|
169
|
+
root?.message,
|
|
172
170
|
typeof value === "string" ? value : undefined,
|
|
173
171
|
);
|
|
174
172
|
const message = rawMessage?.slice(0, CODEX_ERROR_MESSAGE_LIMIT);
|
|
@@ -183,16 +181,27 @@ export function classifyCodexFailure(value: unknown, metadata: CodexFailureMetad
|
|
|
183
181
|
if (matches(evidence, ["insufficient_quota", "quota exceeded", "out of credits", "billing"])) {
|
|
184
182
|
return { kind: "quota", transient: false, ...base };
|
|
185
183
|
}
|
|
186
|
-
if (
|
|
184
|
+
if (
|
|
185
|
+
metadata.status === 401 ||
|
|
186
|
+
metadata.status === 403 ||
|
|
187
|
+
matches(evidence, ["invalid_api_key", "authentication", "unauthorized", "permission_denied"])
|
|
188
|
+
) {
|
|
187
189
|
return { kind: "authentication", transient: false, ...base };
|
|
188
190
|
}
|
|
189
|
-
if (
|
|
191
|
+
if (
|
|
192
|
+
matches(evidence, ["invalid_prompt", "invalid_request", "context_length_exceeded"]) ||
|
|
193
|
+
metadata.status === 400 ||
|
|
194
|
+
metadata.status === 422
|
|
195
|
+
) {
|
|
190
196
|
return { kind: "invalid-request", transient: false, ...base };
|
|
191
197
|
}
|
|
192
198
|
if (matches(evidence, ["concurrency_limit", "too many concurrent requests", "throttled"])) {
|
|
193
199
|
return { kind: "concurrency", transient: true, ...base };
|
|
194
200
|
}
|
|
195
|
-
if (
|
|
201
|
+
if (
|
|
202
|
+
matches(evidence, ["server_is_overloaded", "slow_down", "service unavailable", "overloaded"]) ||
|
|
203
|
+
(metadata.status !== undefined && metadata.status >= 500 && metadata.status <= 599)
|
|
204
|
+
) {
|
|
196
205
|
return { kind: "overload", transient: true, ...base };
|
|
197
206
|
}
|
|
198
207
|
if (metadata.status === 429 || matches(evidence, ["rate_limit_exceeded", "rate limit", "too many requests"])) {
|
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
CONTEXT_HUB_CONTRIBUTION_MAX_AGE_MS,
|
|
18
18
|
CONTEXT_HUB_CONTRIBUTION_SCHEMA,
|
|
19
19
|
CONTEXT_HUB_ITEM_LABEL_MAX_CHARACTERS,
|
|
20
|
-
CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT,
|
|
21
20
|
CONTEXT_HUB_MAX_ITEM_DEPTH,
|
|
21
|
+
CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT,
|
|
22
22
|
CONTEXT_HUB_PRODUCER_NAME_MAX_CHARACTERS,
|
|
23
23
|
CONTEXT_HUB_SEGMENT_KEY_MAX_CHARACTERS,
|
|
24
24
|
CONTEXT_HUB_SEGMENT_LABEL_MAX_CHARACTERS,
|
|
@@ -58,12 +58,14 @@ export interface ContextContribution {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function nonEmptyString(value: unknown, name: string, maxLength: number): string {
|
|
61
|
-
if (typeof value !== "string" || value.length === 0 || value.length > maxLength)
|
|
61
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength)
|
|
62
|
+
throw new Error(`${name} must be a non-empty string of at most ${maxLength} characters`);
|
|
62
63
|
return value;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
function boundedInteger(value: unknown, name: string): number {
|
|
66
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
67
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
68
|
+
throw new Error(`${name} must be a bounded non-negative integer`);
|
|
67
69
|
return value;
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -76,15 +78,16 @@ function validateSegmentItem(value: unknown, depth: number): ContextSegmentItem
|
|
|
76
78
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context segment item must be an object");
|
|
77
79
|
const input = value as Record<string, unknown>;
|
|
78
80
|
for (const key of Object.keys(input)) {
|
|
79
|
-
if (key !== "label" && key !== "estimatedTokens" && key !== "children")
|
|
81
|
+
if (key !== "label" && key !== "estimatedTokens" && key !== "children")
|
|
82
|
+
throw new Error(`context segment item contains unexpected field: ${key}`);
|
|
80
83
|
}
|
|
81
84
|
const item: ContextSegmentItem = {
|
|
82
|
-
label: nonEmptyString(input
|
|
83
|
-
estimatedTokens: boundedInteger(input
|
|
85
|
+
label: nonEmptyString(input.label, "item.label", CONTEXT_HUB_ITEM_LABEL_MAX_CHARACTERS),
|
|
86
|
+
estimatedTokens: boundedInteger(input.estimatedTokens, "item.estimatedTokens"),
|
|
84
87
|
};
|
|
85
|
-
if (input
|
|
86
|
-
if (!Array.isArray(input
|
|
87
|
-
item.children = input
|
|
88
|
+
if (input.children !== undefined) {
|
|
89
|
+
if (!Array.isArray(input.children)) throw new Error("item.children must be an array");
|
|
90
|
+
item.children = input.children.map((child) => validateSegmentItem(child, depth + 1));
|
|
88
91
|
}
|
|
89
92
|
return item;
|
|
90
93
|
}
|
|
@@ -98,22 +101,23 @@ export function validateContextSegment(value: unknown): ContextSegment {
|
|
|
98
101
|
throw new Error(`context segment contains unexpected field: ${key}`);
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
|
-
const confidence = input
|
|
104
|
+
const confidence = input.confidence;
|
|
102
105
|
if (typeof confidence !== "string" || !CONTEXT_HUB_CONFIDENCE_TIERS.includes(confidence as ContextConfidenceTier)) {
|
|
103
106
|
throw new Error(`confidence must be one of ${CONTEXT_HUB_CONFIDENCE_TIERS.join(", ")}`);
|
|
104
107
|
}
|
|
105
|
-
if (input
|
|
108
|
+
if (input.unknown !== undefined && typeof input.unknown !== "boolean") throw new Error("segment.unknown must be a boolean");
|
|
106
109
|
const segment: ContextSegment = {
|
|
107
|
-
key: nonEmptyString(input
|
|
108
|
-
label: nonEmptyString(input
|
|
109
|
-
estimatedTokens: boundedInteger(input
|
|
110
|
+
key: nonEmptyString(input.key, "segment.key", CONTEXT_HUB_SEGMENT_KEY_MAX_CHARACTERS),
|
|
111
|
+
label: nonEmptyString(input.label, "segment.label", CONTEXT_HUB_SEGMENT_LABEL_MAX_CHARACTERS),
|
|
112
|
+
estimatedTokens: boundedInteger(input.estimatedTokens, "segment.estimatedTokens"),
|
|
110
113
|
confidence: confidence as ContextConfidenceTier,
|
|
111
|
-
...(input
|
|
114
|
+
...(input.unknown !== undefined ? { unknown: input.unknown as boolean } : {}),
|
|
112
115
|
};
|
|
113
|
-
if (input
|
|
114
|
-
if (!Array.isArray(input
|
|
115
|
-
const items = input
|
|
116
|
-
if (countItems(items) > CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT)
|
|
116
|
+
if (input.items !== undefined) {
|
|
117
|
+
if (!Array.isArray(input.items)) throw new Error("segment.items must be an array");
|
|
118
|
+
const items = input.items.map((item) => validateSegmentItem(item, 1));
|
|
119
|
+
if (countItems(items) > CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT)
|
|
120
|
+
throw new Error(`segment.items exceeds ${CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT} total items`);
|
|
117
121
|
segment.items = items;
|
|
118
122
|
}
|
|
119
123
|
return segment;
|
|
@@ -125,16 +129,17 @@ const CONTRIBUTION_FIELDS = new Set(["schema", "observedAt", "sequence", "produc
|
|
|
125
129
|
export function validateContextContribution(value: unknown, now = Date.now()): ContextContribution {
|
|
126
130
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context contribution must be an object");
|
|
127
131
|
const input = value as Record<string, unknown>;
|
|
128
|
-
for (const key of Object.keys(input))
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
for (const key of Object.keys(input))
|
|
133
|
+
if (!CONTRIBUTION_FIELDS.has(key)) throw new Error(`context contribution contains unexpected field: ${key}`);
|
|
134
|
+
if (input.schema !== CONTEXT_HUB_CONTRIBUTION_SCHEMA) throw new Error("context contribution schema is not supported");
|
|
135
|
+
const observedAt = boundedInteger(input.observedAt, "observedAt");
|
|
131
136
|
if (Math.abs(now - observedAt) > CONTEXT_HUB_CONTRIBUTION_MAX_AGE_MS) throw new Error("context contribution is stale");
|
|
132
137
|
return {
|
|
133
138
|
schema: CONTEXT_HUB_CONTRIBUTION_SCHEMA,
|
|
134
139
|
observedAt,
|
|
135
|
-
sequence: boundedInteger(input
|
|
136
|
-
producerName: nonEmptyString(input
|
|
137
|
-
segment: validateContextSegment(input
|
|
140
|
+
sequence: boundedInteger(input.sequence, "sequence"),
|
|
141
|
+
producerName: nonEmptyString(input.producerName, "producerName", CONTEXT_HUB_PRODUCER_NAME_MAX_CHARACTERS),
|
|
142
|
+
segment: validateContextSegment(input.segment),
|
|
138
143
|
};
|
|
139
144
|
}
|
|
140
145
|
|
|
@@ -199,7 +204,11 @@ export function computeToolSchemaLedger(tools: readonly ToolLedgerEntry[]): Tool
|
|
|
199
204
|
for (const tool of tools) {
|
|
200
205
|
const source = tool.sourceInfo?.source && tool.sourceInfo.source.length > 0 ? tool.sourceInfo.source : "unknown";
|
|
201
206
|
const characters = toolCharacters(tool);
|
|
202
|
-
const usage: ToolLedgerToolUsage = {
|
|
207
|
+
const usage: ToolLedgerToolUsage = {
|
|
208
|
+
name: tool.name,
|
|
209
|
+
characters,
|
|
210
|
+
estimatedTokens: Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN),
|
|
211
|
+
};
|
|
203
212
|
const existing = bySource.get(source);
|
|
204
213
|
if (existing) existing.push(usage);
|
|
205
214
|
else bySource.set(source, [usage]);
|