@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.
- package/README.md +38 -13
- package/docs/COMPACTION_EFFECTIVENESS.md +34 -0
- package/docs/CONTEXT_SNAPSHOTS.md +53 -0
- package/docs/MODEL_CATALOG.md +43 -0
- package/docs/OTLP_EXPORT.md +63 -0
- package/docs/PROVIDER_RESEARCH.md +1 -1
- package/docs/TOKEN_MEASUREMENT.md +53 -0
- package/docs/USAGE_BACKFILL.md +35 -0
- package/package.json +4 -3
- package/src/{providers/anthropic-contracts.ts → anthropic/rate-limits.ts} +1 -1
- package/src/{adapters/artificial-analysis-direct-source.ts → artificial-analysis/benchmark-source.ts} +3 -3
- package/src/cli-commands/backfill.ts +51 -0
- package/src/cli-commands/benchmarks.ts +4 -4
- package/src/cli-commands/catalog.ts +123 -0
- package/src/cli-commands/compaction.ts +1 -1
- package/src/cli-commands/context.ts +77 -4
- package/src/cli-commands/export.ts +30 -0
- package/src/cli-commands/metrics.ts +8 -2
- package/src/cli-commands/op.ts +1 -1
- package/src/cli-commands/route-args.ts +2 -2
- package/src/cli-commands/router.ts +2 -2
- package/src/cli-commands/support.ts +2 -2
- package/src/cli.ts +54 -21
- package/src/{providers/codex-contracts.ts → codex/contracts.ts} +1 -1
- package/src/codex/source.ts +53 -0
- package/src/{providers/codex.ts → codex/telemetry.ts} +2 -2
- package/src/constants.ts +18 -0
- package/src/daemon.ts +53 -23
- package/src/{providers/google-vertex-budget-contracts.ts → google-vertex/budget-contracts.ts} +3 -3
- package/src/{providers/google-vertex-budget.ts → google-vertex/budget-telemetry.ts} +6 -6
- package/src/{providers/google-vertex-contracts.ts → google-vertex/failures.ts} +1 -1
- package/src/google-vertex/source.ts +35 -0
- package/src/index.ts +135 -55
- package/src/{adapters/lmarena-hf-source.ts → lmarena/benchmark-source.ts} +3 -3
- package/src/observability/budget.ts +14 -0
- package/src/observability/context-delta.ts +309 -0
- package/src/{domain → observability}/context-hub.ts +19 -2
- package/src/observability/context-snapshot-history.ts +281 -0
- package/src/{domain → observability}/context-telemetry.ts +232 -4
- package/src/observability/model-identity.ts +42 -0
- package/src/{domain → observability}/model-observation.ts +26 -11
- package/src/{ports/metric-store.ts → observability/store.ts} +2 -2
- package/src/{ports → observability}/telemetry-source.ts +2 -2
- package/src/observability/token-measurement.ts +194 -0
- package/src/observability/usage-import.ts +122 -0
- package/src/openai/token-counter.ts +63 -0
- package/src/{adapters/openrouter-benchmark-source.ts → openrouter/benchmark-source.ts} +3 -3
- package/src/{providers/openrouter-contracts.ts → openrouter/contracts.ts} +1 -1
- package/src/{adapters/openrouter-design-arena-source.ts → openrouter/design-arena-source.ts} +4 -4
- package/src/openrouter/source.ts +20 -0
- package/src/{providers/openrouter.ts → openrouter/telemetry.ts} +2 -2
- package/src/{domain → optimization/model-selection}/benchmark.ts +9 -27
- package/src/optimization/model-selection/catalog-store.ts +97 -0
- package/src/optimization/model-selection/catalog.ts +478 -0
- package/src/{ports/benchmark-controller.ts → optimization/model-selection/controller.ts} +1 -1
- package/src/{adapters/metric-benchmark-store.ts → optimization/model-selection/observation-store.ts} +5 -5
- package/src/{domain/model-ranking-service.ts → optimization/model-selection/ranker.ts} +6 -12
- package/src/{domain/model-ranking.ts → optimization/model-selection/ranking.ts} +8 -2
- package/src/{ports/benchmark-source.ts → optimization/model-selection/source.ts} +1 -1
- package/src/{ports/benchmark-store.ts → optimization/model-selection/store.ts} +1 -1
- package/src/{domain/codex-recovery.ts → optimization/recovery/codex.ts} +1 -1
- package/src/{ports/router-controller.ts → optimization/routing/controller.ts} +1 -1
- package/src/{policy.ts → optimization/routing/policy.ts} +4 -14
- package/src/{router.ts → optimization/routing/router.ts} +4 -10
- package/src/otlp/exporter.ts +354 -0
- package/src/pi/session-usage-source.ts +203 -0
- package/src/{session-identity-service.ts → sessions/identity.ts} +2 -2
- package/src/{operations/session-scope.ts → sessions/router-authorization.ts} +1 -1
- package/src/{db.ts → sqlite/database.ts} +14 -1
- package/src/{adapters/sqlite-metric-store.ts → sqlite/metric-store.ts} +4 -4
- package/src/{adapters/sqlite-session-identity-store.ts → sqlite/session-store.ts} +1 -1
- package/src/sqlite/usage-import-store.ts +133 -0
- package/src/telemetry-export/exporter.ts +84 -0
- package/src/{operations → vehicle}/benchmark-operations.ts +3 -3
- package/src/vehicle/catalog-operations.ts +10 -0
- package/src/{client.ts → vehicle/client.ts} +1 -1
- package/src/{operations → vehicle}/context-operations.ts +13 -5
- package/src/vehicle/export-operations.ts +12 -0
- package/src/{operations/metrics-operations.ts → vehicle/metric-operations.ts} +4 -4
- package/src/{operations → vehicle}/model-ranking-operations.ts +3 -3
- package/src/{vehicle-registration.ts → vehicle/registration.ts} +14 -4
- package/src/{operations/router-operations.ts → vehicle/routing-operations.ts} +4 -4
- package/src/{service.ts → vehicle/service.ts} +108 -26
- package/src/{operations/session-identity-operations.ts → vehicle/session-operations.ts} +3 -3
- package/src/vehicle/usage-import-operations.ts +10 -0
- package/src/providers/telemetry-sources.ts +0 -108
- /package/src/{providers/google-adc-auth.ts → google-vertex/auth.ts} +0 -0
- /package/src/{domain → observability}/metric.ts +0 -0
- /package/src/{domain → observability}/task-cost.ts +0 -0
- /package/src/{domain → observability}/task-focus.ts +0 -0
- /package/src/{domain → observability}/usage.ts +0 -0
- /package/src/{config.ts → optimization/routing/config.ts} +0 -0
- /package/src/{ports/session-identity-store.ts → sessions/store.ts} +0 -0
- /package/src/{operations/types.ts → vehicle/operation-types.ts} +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { normalizeModelIdentity } from "./model-identity.ts";
|
|
3
|
+
|
|
4
|
+
export const CONTEXT_SEGMENT_SOURCES = [
|
|
5
|
+
"base-prompt",
|
|
6
|
+
"tool-definitions",
|
|
7
|
+
"rules",
|
|
8
|
+
"skills",
|
|
9
|
+
"project-context",
|
|
10
|
+
"conversation-message",
|
|
11
|
+
"thinking",
|
|
12
|
+
"tool-call",
|
|
13
|
+
"tool-result",
|
|
14
|
+
] as const;
|
|
15
|
+
export type ContextSegmentSource = (typeof CONTEXT_SEGMENT_SOURCES)[number];
|
|
16
|
+
|
|
17
|
+
export const CONTEXT_SEGMENT_STATES = ["active", "compacted", "inactive"] as const;
|
|
18
|
+
export type ContextSegmentState = (typeof CONTEXT_SEGMENT_STATES)[number];
|
|
19
|
+
|
|
20
|
+
export const CONTEXT_SNAPSHOT_MAX_SEGMENTS = 512;
|
|
21
|
+
const OPAQUE_FINGERPRINT = /^[A-Za-z0-9_-]{32,64}$/;
|
|
22
|
+
|
|
23
|
+
export interface ContextSnapshotSegment {
|
|
24
|
+
/** Keyed opaque logical identity. It must not contain an entry id, path, label, or content. */
|
|
25
|
+
id: string;
|
|
26
|
+
/** Keyed opaque fingerprint of the ephemeral segment value. */
|
|
27
|
+
fingerprint: string;
|
|
28
|
+
source: ContextSegmentSource;
|
|
29
|
+
tokens: number;
|
|
30
|
+
state: ContextSegmentState;
|
|
31
|
+
/** Position in the final provider request's stable-prefix sequence, or null for historical-only segments. */
|
|
32
|
+
requestPosition: number | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ContextSnapshot {
|
|
36
|
+
version: 1;
|
|
37
|
+
snapshotId: string;
|
|
38
|
+
sessionId: string;
|
|
39
|
+
provider: string;
|
|
40
|
+
model: string;
|
|
41
|
+
capturedAt: number;
|
|
42
|
+
truncated: boolean;
|
|
43
|
+
segments: ContextSnapshotSegment[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ContextSegmentLifecycle = "added" | "retained" | "changed" | "evicted" | "compacted" | "inactive";
|
|
47
|
+
|
|
48
|
+
export interface ContextSegmentChange {
|
|
49
|
+
id: string;
|
|
50
|
+
source: ContextSegmentSource;
|
|
51
|
+
lifecycle: ContextSegmentLifecycle;
|
|
52
|
+
previousTokens: number;
|
|
53
|
+
currentTokens: number;
|
|
54
|
+
deltaTokens: number;
|
|
55
|
+
requestPosition: number | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ContextSourceGrowth {
|
|
59
|
+
source: ContextSegmentSource;
|
|
60
|
+
deltaTokens: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type ContextPrefixResetReason = "initial" | "session-changed" | "provider-changed" | "model-changed" | null;
|
|
64
|
+
|
|
65
|
+
export interface ContextDelta {
|
|
66
|
+
previousSnapshotId: string | null;
|
|
67
|
+
currentSnapshotId: string;
|
|
68
|
+
capturedAt: number;
|
|
69
|
+
truncated: boolean;
|
|
70
|
+
stablePrefixTokens: number;
|
|
71
|
+
firstChangedSegment: Pick<ContextSnapshotSegment, "id" | "source" | "requestPosition"> | null;
|
|
72
|
+
resetReason: ContextPrefixResetReason;
|
|
73
|
+
changes: ContextSegmentChange[];
|
|
74
|
+
growthBySource: ContextSourceGrowth[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const SNAPSHOT_FIELDS = new Set<keyof ContextSnapshot>([
|
|
78
|
+
"version",
|
|
79
|
+
"snapshotId",
|
|
80
|
+
"sessionId",
|
|
81
|
+
"provider",
|
|
82
|
+
"model",
|
|
83
|
+
"capturedAt",
|
|
84
|
+
"truncated",
|
|
85
|
+
"segments",
|
|
86
|
+
]);
|
|
87
|
+
const SEGMENT_FIELDS = new Set<keyof ContextSnapshotSegment>(["id", "fingerprint", "source", "tokens", "state", "requestPosition"]);
|
|
88
|
+
|
|
89
|
+
function opaqueFingerprint(value: unknown, name: string): string {
|
|
90
|
+
if (typeof value !== "string" || !OPAQUE_FINGERPRINT.test(value)) throw new Error(`${name} fingerprint is invalid`);
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function nonNegativeInteger(value: unknown, name: string): number {
|
|
95
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`${name} is invalid`);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateSegment(value: unknown): ContextSnapshotSegment {
|
|
100
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context snapshot segment must be an object");
|
|
101
|
+
const input = value as Record<string, unknown>;
|
|
102
|
+
for (const key of Object.keys(input)) {
|
|
103
|
+
if (!SEGMENT_FIELDS.has(key as keyof ContextSnapshotSegment))
|
|
104
|
+
throw new Error(`context snapshot segment contains unsupported field: ${key}`);
|
|
105
|
+
}
|
|
106
|
+
if (!CONTEXT_SEGMENT_SOURCES.includes(input.source as ContextSegmentSource))
|
|
107
|
+
throw new Error("context snapshot segment source is invalid");
|
|
108
|
+
if (!CONTEXT_SEGMENT_STATES.includes(input.state as ContextSegmentState)) throw new Error("context snapshot segment state is invalid");
|
|
109
|
+
const requestPosition = input.requestPosition;
|
|
110
|
+
if (requestPosition !== null && (typeof requestPosition !== "number" || !Number.isSafeInteger(requestPosition) || requestPosition < 0))
|
|
111
|
+
throw new Error("context snapshot segment request position is invalid");
|
|
112
|
+
return {
|
|
113
|
+
id: opaqueFingerprint(input.id, "segment id"),
|
|
114
|
+
fingerprint: opaqueFingerprint(input.fingerprint, "segment"),
|
|
115
|
+
source: input.source as ContextSegmentSource,
|
|
116
|
+
tokens: nonNegativeInteger(input.tokens, "context snapshot segment tokens"),
|
|
117
|
+
state: input.state as ContextSegmentState,
|
|
118
|
+
requestPosition: requestPosition as number | null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Strict, content-free ingress validation before a snapshot can be compared or persisted. */
|
|
123
|
+
export function validateContextSnapshot(value: unknown): ContextSnapshot {
|
|
124
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context snapshot must be an object");
|
|
125
|
+
const input = value as Record<string, unknown>;
|
|
126
|
+
for (const key of Object.keys(input)) {
|
|
127
|
+
if (!SNAPSHOT_FIELDS.has(key as keyof ContextSnapshot)) throw new Error(`context snapshot contains unsupported field: ${key}`);
|
|
128
|
+
}
|
|
129
|
+
if (input.version !== 1) throw new Error("context snapshot version is unsupported");
|
|
130
|
+
if (!Array.isArray(input.segments)) throw new Error("context snapshot segments must be an array");
|
|
131
|
+
if (input.segments.length > CONTEXT_SNAPSHOT_MAX_SEGMENTS) throw new Error("context snapshot exceeds the segment limit");
|
|
132
|
+
const segments = input.segments.map(validateSegment);
|
|
133
|
+
const ids = new Set<string>();
|
|
134
|
+
const requestPositions = new Set<number>();
|
|
135
|
+
for (const segment of segments) {
|
|
136
|
+
if (ids.has(segment.id)) throw new Error("context snapshot contains a duplicate segment id");
|
|
137
|
+
ids.add(segment.id);
|
|
138
|
+
if (segment.requestPosition !== null) {
|
|
139
|
+
if (requestPositions.has(segment.requestPosition)) throw new Error("context snapshot contains a duplicate request position");
|
|
140
|
+
requestPositions.add(segment.requestPosition);
|
|
141
|
+
if (segment.state !== "active") throw new Error("historical context segment cannot have a request position");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (typeof input.provider !== "string" || typeof input.model !== "string") throw new Error("context snapshot model identity is invalid");
|
|
145
|
+
const identity = normalizeModelIdentity(input.provider, input.model);
|
|
146
|
+
return {
|
|
147
|
+
version: 1,
|
|
148
|
+
snapshotId: opaqueFingerprint(input.snapshotId, "snapshot id"),
|
|
149
|
+
sessionId: opaqueFingerprint(input.sessionId, "session id"),
|
|
150
|
+
provider: identity.provider,
|
|
151
|
+
model: identity.model,
|
|
152
|
+
capturedAt: nonNegativeInteger(input.capturedAt, "context snapshot capture time"),
|
|
153
|
+
truncated:
|
|
154
|
+
input.truncated === false
|
|
155
|
+
? false
|
|
156
|
+
: input.truncated === true
|
|
157
|
+
? true
|
|
158
|
+
: (() => {
|
|
159
|
+
throw new Error("context snapshot truncated flag is invalid");
|
|
160
|
+
})(),
|
|
161
|
+
segments,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resetReason(previous: ContextSnapshot | null, current: ContextSnapshot): ContextPrefixResetReason {
|
|
166
|
+
if (!previous) return "initial";
|
|
167
|
+
if (previous.sessionId !== current.sessionId) return "session-changed";
|
|
168
|
+
if (previous.provider !== current.provider) return "provider-changed";
|
|
169
|
+
if (previous.model !== current.model) return "model-changed";
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function requestSegments(snapshot: ContextSnapshot): ContextSnapshotSegment[] {
|
|
174
|
+
return snapshot.segments
|
|
175
|
+
.filter((segment): segment is ContextSnapshotSegment & { requestPosition: number } => segment.requestPosition !== null)
|
|
176
|
+
.sort((left, right) => left.requestPosition - right.requestPosition);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function sameRequestSegment(left: ContextSnapshotSegment, right: ContextSnapshotSegment): boolean {
|
|
180
|
+
return (
|
|
181
|
+
left.id === right.id &&
|
|
182
|
+
left.fingerprint === right.fingerprint &&
|
|
183
|
+
left.source === right.source &&
|
|
184
|
+
left.tokens === right.tokens &&
|
|
185
|
+
left.state === right.state &&
|
|
186
|
+
left.requestPosition === right.requestPosition
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function prefixEvidence(
|
|
191
|
+
previous: ContextSnapshot | null,
|
|
192
|
+
current: ContextSnapshot,
|
|
193
|
+
reset: ContextPrefixResetReason,
|
|
194
|
+
): Pick<ContextDelta, "stablePrefixTokens" | "firstChangedSegment"> {
|
|
195
|
+
const previousRequest = previous ? requestSegments(previous) : [];
|
|
196
|
+
const currentRequest = requestSegments(current);
|
|
197
|
+
if (reset !== null) {
|
|
198
|
+
const first = currentRequest[0] ?? previousRequest[0];
|
|
199
|
+
return {
|
|
200
|
+
stablePrefixTokens: 0,
|
|
201
|
+
firstChangedSegment: first ? { id: first.id, source: first.source, requestPosition: first.requestPosition } : null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
let stablePrefixTokens = 0;
|
|
205
|
+
const limit = Math.max(previousRequest.length, currentRequest.length);
|
|
206
|
+
for (let index = 0; index < limit; index++) {
|
|
207
|
+
const before = previousRequest[index];
|
|
208
|
+
const after = currentRequest[index];
|
|
209
|
+
if (before && after && sameRequestSegment(before, after)) {
|
|
210
|
+
stablePrefixTokens += after.tokens;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const first = after ?? before;
|
|
214
|
+
return {
|
|
215
|
+
stablePrefixTokens,
|
|
216
|
+
firstChangedSegment: first ? { id: first.id, source: first.source, requestPosition: first.requestPosition } : null,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { stablePrefixTokens, firstChangedSegment: null };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function lifecycle(previous: ContextSnapshotSegment | undefined, current: ContextSnapshotSegment): ContextSegmentLifecycle {
|
|
223
|
+
if (current.state === "compacted") return "compacted";
|
|
224
|
+
if (current.state === "inactive") return "inactive";
|
|
225
|
+
if (!previous) return "added";
|
|
226
|
+
return sameRequestSegment(previous, current) ||
|
|
227
|
+
(previous.id === current.id &&
|
|
228
|
+
previous.fingerprint === current.fingerprint &&
|
|
229
|
+
previous.source === current.source &&
|
|
230
|
+
previous.tokens === current.tokens &&
|
|
231
|
+
previous.state === current.state)
|
|
232
|
+
? "retained"
|
|
233
|
+
: "changed";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function sourceGrowth(previous: ContextSnapshot | null, current: ContextSnapshot): ContextSourceGrowth[] {
|
|
237
|
+
const before = new Map<ContextSegmentSource, number>();
|
|
238
|
+
const after = new Map<ContextSegmentSource, number>();
|
|
239
|
+
for (const segment of previous?.segments ?? []) before.set(segment.source, (before.get(segment.source) ?? 0) + segment.tokens);
|
|
240
|
+
for (const segment of current.segments) after.set(segment.source, (after.get(segment.source) ?? 0) + segment.tokens);
|
|
241
|
+
return CONTEXT_SEGMENT_SOURCES.filter((source) => before.has(source) || after.has(source)).map((source) => ({
|
|
242
|
+
source,
|
|
243
|
+
deltaTokens: (after.get(source) ?? 0) - (before.get(source) ?? 0),
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Correlates bounded keyed identities. Stable-prefix correlation is evidence of structural churn,
|
|
249
|
+
* not proof of a provider cache hit or miss.
|
|
250
|
+
*/
|
|
251
|
+
export function compareContextSnapshots(previousValue: ContextSnapshot | null, currentValue: ContextSnapshot): ContextDelta {
|
|
252
|
+
const previous = previousValue === null ? null : validateContextSnapshot(previousValue);
|
|
253
|
+
const current = validateContextSnapshot(currentValue);
|
|
254
|
+
const priorById = new Map((previous?.segments ?? []).map((segment) => [segment.id, segment]));
|
|
255
|
+
const changes: ContextSegmentChange[] = current.segments.map((segment) => {
|
|
256
|
+
const prior = priorById.get(segment.id);
|
|
257
|
+
priorById.delete(segment.id);
|
|
258
|
+
return {
|
|
259
|
+
id: segment.id,
|
|
260
|
+
source: segment.source,
|
|
261
|
+
lifecycle: lifecycle(prior, segment),
|
|
262
|
+
previousTokens: prior?.tokens ?? 0,
|
|
263
|
+
currentTokens: segment.tokens,
|
|
264
|
+
deltaTokens: segment.tokens - (prior?.tokens ?? 0),
|
|
265
|
+
requestPosition: segment.requestPosition,
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
for (const segment of previous?.segments ?? []) {
|
|
269
|
+
if (!priorById.has(segment.id)) continue;
|
|
270
|
+
changes.push({
|
|
271
|
+
id: segment.id,
|
|
272
|
+
source: segment.source,
|
|
273
|
+
lifecycle: "evicted",
|
|
274
|
+
previousTokens: segment.tokens,
|
|
275
|
+
currentTokens: 0,
|
|
276
|
+
deltaTokens: -segment.tokens,
|
|
277
|
+
requestPosition: null,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const reset = resetReason(previous, current);
|
|
281
|
+
return {
|
|
282
|
+
previousSnapshotId: previous?.snapshotId ?? null,
|
|
283
|
+
currentSnapshotId: current.snapshotId,
|
|
284
|
+
capturedAt: current.capturedAt,
|
|
285
|
+
truncated: current.truncated,
|
|
286
|
+
...prefixEvidence(previous, current, reset),
|
|
287
|
+
resetReason: reset,
|
|
288
|
+
changes,
|
|
289
|
+
growthBySource: sourceGrowth(previous, current),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface ContextFingerprinter {
|
|
294
|
+
fingerprint(value: string | Uint8Array): string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** HMAC keeps low-entropy/context labels resistant to offline reversal if snapshot rows are copied. */
|
|
298
|
+
export class HmacContextFingerprinter implements ContextFingerprinter {
|
|
299
|
+
private readonly key: Uint8Array;
|
|
300
|
+
|
|
301
|
+
constructor(key: Uint8Array) {
|
|
302
|
+
if (key.byteLength < 32) throw new Error("context fingerprint key must contain at least 32 bytes");
|
|
303
|
+
this.key = key.slice();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
fingerprint(value: string | Uint8Array): string {
|
|
307
|
+
return createHmac("sha256", this.key).update(value).digest().subarray(0, 24).toString("base64url");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -24,12 +24,22 @@ import {
|
|
|
24
24
|
CONTEXT_HUB_SEGMENT_LABEL_MAX_CHARACTERS,
|
|
25
25
|
} from "../constants.ts";
|
|
26
26
|
import type { MetricObservation } from "./metric.ts";
|
|
27
|
+
import {
|
|
28
|
+
type RequestTokenReconciliation,
|
|
29
|
+
type TokenMeasurement,
|
|
30
|
+
validateRequestTokenReconciliation,
|
|
31
|
+
validateTokenMeasurement,
|
|
32
|
+
} from "./token-measurement.ts";
|
|
27
33
|
|
|
28
34
|
export type ContextConfidenceTier = (typeof CONTEXT_HUB_CONFIDENCE_TIERS)[number];
|
|
29
35
|
|
|
30
36
|
export interface ContextSegmentItem {
|
|
31
37
|
label: string;
|
|
32
38
|
estimatedTokens: number;
|
|
39
|
+
/** Explicit provenance for this item's own token count. Omitted only by legacy/cooperative producers that have not migrated yet. */
|
|
40
|
+
measurement?: TokenMeasurement;
|
|
41
|
+
/** Provider aggregate for the request associated with this entry, kept separate from this item's own cost. */
|
|
42
|
+
requestTokenReconciliation?: RequestTokenReconciliation;
|
|
33
43
|
children?: ContextSegmentItem[];
|
|
34
44
|
}
|
|
35
45
|
|
|
@@ -78,12 +88,19 @@ function validateSegmentItem(value: unknown, depth: number): ContextSegmentItem
|
|
|
78
88
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context segment item must be an object");
|
|
79
89
|
const input = value as Record<string, unknown>;
|
|
80
90
|
for (const key of Object.keys(input)) {
|
|
81
|
-
if (key !== "label" && key !== "estimatedTokens" && key !== "children")
|
|
91
|
+
if (key !== "label" && key !== "estimatedTokens" && key !== "measurement" && key !== "requestTokenReconciliation" && key !== "children")
|
|
82
92
|
throw new Error(`context segment item contains unexpected field: ${key}`);
|
|
83
93
|
}
|
|
94
|
+
const estimatedTokens = boundedInteger(input.estimatedTokens, "item.estimatedTokens");
|
|
95
|
+
const measurement = input.measurement === undefined ? undefined : validateTokenMeasurement(input.measurement);
|
|
96
|
+
if (measurement && measurement.tokens !== estimatedTokens) throw new Error("item measurement tokens must match item.estimatedTokens");
|
|
97
|
+
const requestTokenReconciliation =
|
|
98
|
+
input.requestTokenReconciliation === undefined ? undefined : validateRequestTokenReconciliation(input.requestTokenReconciliation);
|
|
84
99
|
const item: ContextSegmentItem = {
|
|
85
100
|
label: nonEmptyString(input.label, "item.label", CONTEXT_HUB_ITEM_LABEL_MAX_CHARACTERS),
|
|
86
|
-
estimatedTokens
|
|
101
|
+
estimatedTokens,
|
|
102
|
+
...(measurement ? { measurement } : {}),
|
|
103
|
+
...(requestTokenReconciliation ? { requestTokenReconciliation } : {}),
|
|
87
104
|
};
|
|
88
105
|
if (input.children !== undefined) {
|
|
89
106
|
if (!Array.isArray(input.children)) throw new Error("item.children must be an array");
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTEXT_SEGMENT_SOURCES,
|
|
3
|
+
CONTEXT_SNAPSHOT_MAX_SEGMENTS,
|
|
4
|
+
type ContextDelta,
|
|
5
|
+
type ContextPrefixResetReason,
|
|
6
|
+
type ContextSegmentChange,
|
|
7
|
+
type ContextSnapshot,
|
|
8
|
+
type ContextSnapshotSegment,
|
|
9
|
+
type ContextSourceGrowth,
|
|
10
|
+
compareContextSnapshots,
|
|
11
|
+
validateContextSnapshot,
|
|
12
|
+
} from "./context-delta.ts";
|
|
13
|
+
import type { MetricObservation, StoredMetricObservation } from "./metric.ts";
|
|
14
|
+
import type { MetricStore } from "./store.ts";
|
|
15
|
+
|
|
16
|
+
const CONTEXT_SNAPSHOT_METRIC_SOURCE = "pi-context-snapshot";
|
|
17
|
+
const CONTEXT_SNAPSHOT_HEADER_METRIC = "snapshot";
|
|
18
|
+
const CONTEXT_SNAPSHOT_SEGMENT_METRIC = "segment";
|
|
19
|
+
const CONTEXT_SNAPSHOT_CHANGE_METRIC = "change";
|
|
20
|
+
const CONTEXT_SNAPSHOT_MAX_CHANGES = CONTEXT_SNAPSHOT_MAX_SEGMENTS * 2;
|
|
21
|
+
|
|
22
|
+
interface SnapshotHeaderAttributes {
|
|
23
|
+
snapshotId: string;
|
|
24
|
+
previousSnapshotId: string | null;
|
|
25
|
+
provider: string;
|
|
26
|
+
model: string;
|
|
27
|
+
segmentCount: number;
|
|
28
|
+
changeCount: number;
|
|
29
|
+
truncated: boolean;
|
|
30
|
+
resetReason: ContextPrefixResetReason;
|
|
31
|
+
firstChangedSegment: ContextDelta["firstChangedSegment"];
|
|
32
|
+
growthBySource: ContextSourceGrowth[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ContextSnapshotHistory {
|
|
36
|
+
record(snapshot: ContextSnapshot): ContextDelta;
|
|
37
|
+
latestSnapshot(sessionId: string): ContextSnapshot | null;
|
|
38
|
+
latestDelta(sessionId: string): ContextDelta | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function recordValue(value: unknown, name: string): Record<string, unknown> {
|
|
42
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${name} is invalid`);
|
|
43
|
+
return value as Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function integer(value: unknown, name: string, maximum = Number.MAX_SAFE_INTEGER): number {
|
|
47
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > maximum) throw new Error(`${name} is invalid`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function nullableFingerprint(value: unknown, name: string): string | null {
|
|
52
|
+
if (value === null) return null;
|
|
53
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{32,64}$/.test(value)) throw new Error(`${name} is invalid`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function headerAttributes(row: StoredMetricObservation): SnapshotHeaderAttributes {
|
|
58
|
+
const input = row.attributes;
|
|
59
|
+
if (typeof input.provider !== "string" || typeof input.model !== "string") throw new Error("stored context snapshot identity is invalid");
|
|
60
|
+
const resetReason = input.resetReason;
|
|
61
|
+
if (resetReason !== null && !["initial", "session-changed", "provider-changed", "model-changed"].includes(resetReason as string))
|
|
62
|
+
throw new Error("stored context snapshot reset reason is invalid");
|
|
63
|
+
const growth = input.growthBySource;
|
|
64
|
+
if (!Array.isArray(growth) || growth.length > CONTEXT_SEGMENT_SOURCES.length) throw new Error("stored context source growth is invalid");
|
|
65
|
+
const growthBySource = growth.map((value) => {
|
|
66
|
+
const item = recordValue(value, "stored context source growth");
|
|
67
|
+
if (
|
|
68
|
+
!CONTEXT_SEGMENT_SOURCES.includes(item.source as ContextSourceGrowth["source"]) ||
|
|
69
|
+
typeof item.deltaTokens !== "number" ||
|
|
70
|
+
!Number.isSafeInteger(item.deltaTokens)
|
|
71
|
+
)
|
|
72
|
+
throw new Error("stored context source growth is invalid");
|
|
73
|
+
return { source: item.source as ContextSourceGrowth["source"], deltaTokens: item.deltaTokens };
|
|
74
|
+
});
|
|
75
|
+
let firstChangedSegment: ContextDelta["firstChangedSegment"] = null;
|
|
76
|
+
if (input.firstChangedSegment !== null) {
|
|
77
|
+
const first = recordValue(input.firstChangedSegment, "stored first changed context segment");
|
|
78
|
+
if (
|
|
79
|
+
typeof first.id !== "string" ||
|
|
80
|
+
!CONTEXT_SEGMENT_SOURCES.includes(first.source as ContextSnapshotSegment["source"]) ||
|
|
81
|
+
(first.requestPosition !== null && (typeof first.requestPosition !== "number" || !Number.isSafeInteger(first.requestPosition)))
|
|
82
|
+
)
|
|
83
|
+
throw new Error("stored first changed context segment is invalid");
|
|
84
|
+
firstChangedSegment = {
|
|
85
|
+
id: first.id,
|
|
86
|
+
source: first.source as ContextSnapshotSegment["source"],
|
|
87
|
+
requestPosition: first.requestPosition as number | null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
snapshotId: nullableFingerprint(input.snapshotId, "stored snapshot id")!,
|
|
92
|
+
previousSnapshotId: nullableFingerprint(input.previousSnapshotId, "stored previous snapshot id"),
|
|
93
|
+
provider: input.provider,
|
|
94
|
+
model: input.model,
|
|
95
|
+
segmentCount: integer(input.segmentCount, "stored context segment count", CONTEXT_SNAPSHOT_MAX_SEGMENTS),
|
|
96
|
+
changeCount: integer(input.changeCount, "stored context change count", CONTEXT_SNAPSHOT_MAX_CHANGES),
|
|
97
|
+
truncated: input.truncated === true,
|
|
98
|
+
resetReason: resetReason as ContextPrefixResetReason,
|
|
99
|
+
firstChangedSegment,
|
|
100
|
+
growthBySource,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function segmentObservation(snapshot: ContextSnapshot, segment: ContextSnapshotSegment, index: number): MetricObservation {
|
|
105
|
+
return {
|
|
106
|
+
source: CONTEXT_SNAPSHOT_METRIC_SOURCE,
|
|
107
|
+
scope: snapshot.sessionId,
|
|
108
|
+
metric: CONTEXT_SNAPSHOT_SEGMENT_METRIC,
|
|
109
|
+
value: segment.tokens,
|
|
110
|
+
unit: "tokens",
|
|
111
|
+
observedAt: snapshot.capturedAt,
|
|
112
|
+
attributes: {
|
|
113
|
+
snapshotId: snapshot.snapshotId,
|
|
114
|
+
index,
|
|
115
|
+
id: segment.id,
|
|
116
|
+
fingerprint: segment.fingerprint,
|
|
117
|
+
source: segment.source,
|
|
118
|
+
state: segment.state,
|
|
119
|
+
requestPosition: segment.requestPosition,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function changeObservation(snapshot: ContextSnapshot, change: ContextSegmentChange, index: number): MetricObservation {
|
|
125
|
+
return {
|
|
126
|
+
source: CONTEXT_SNAPSHOT_METRIC_SOURCE,
|
|
127
|
+
scope: snapshot.sessionId,
|
|
128
|
+
metric: CONTEXT_SNAPSHOT_CHANGE_METRIC,
|
|
129
|
+
value: change.deltaTokens,
|
|
130
|
+
unit: "tokens",
|
|
131
|
+
observedAt: snapshot.capturedAt,
|
|
132
|
+
attributes: { snapshotId: snapshot.snapshotId, index, ...change },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function headerObservation(snapshot: ContextSnapshot, delta: ContextDelta): MetricObservation {
|
|
137
|
+
return {
|
|
138
|
+
source: CONTEXT_SNAPSHOT_METRIC_SOURCE,
|
|
139
|
+
scope: snapshot.sessionId,
|
|
140
|
+
metric: CONTEXT_SNAPSHOT_HEADER_METRIC,
|
|
141
|
+
value: delta.stablePrefixTokens,
|
|
142
|
+
unit: "tokens",
|
|
143
|
+
observedAt: snapshot.capturedAt,
|
|
144
|
+
attributes: {
|
|
145
|
+
snapshotId: snapshot.snapshotId,
|
|
146
|
+
previousSnapshotId: delta.previousSnapshotId,
|
|
147
|
+
provider: snapshot.provider,
|
|
148
|
+
model: snapshot.model,
|
|
149
|
+
segmentCount: snapshot.segments.length,
|
|
150
|
+
changeCount: delta.changes.length,
|
|
151
|
+
truncated: snapshot.truncated,
|
|
152
|
+
resetReason: delta.resetReason,
|
|
153
|
+
firstChangedSegment: delta.firstChangedSegment,
|
|
154
|
+
growthBySource: delta.growthBySource,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function segmentFromRow(row: StoredMetricObservation): { index: number; segment: ContextSnapshotSegment } {
|
|
160
|
+
const input = row.attributes;
|
|
161
|
+
const requestPosition = input.requestPosition;
|
|
162
|
+
if (row.value === null) throw new Error("stored context segment token count is invalid");
|
|
163
|
+
return {
|
|
164
|
+
index: integer(input.index, "stored context segment index", CONTEXT_SNAPSHOT_MAX_SEGMENTS - 1),
|
|
165
|
+
segment: {
|
|
166
|
+
id: String(input.id),
|
|
167
|
+
fingerprint: String(input.fingerprint),
|
|
168
|
+
source: input.source as ContextSnapshotSegment["source"],
|
|
169
|
+
tokens: integer(row.value, "stored context segment token count"),
|
|
170
|
+
state: input.state as ContextSnapshotSegment["state"],
|
|
171
|
+
requestPosition: requestPosition === null ? null : integer(requestPosition, "stored context request position"),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function changeFromRow(row: StoredMetricObservation): { index: number; change: ContextSegmentChange } {
|
|
177
|
+
const input = row.attributes;
|
|
178
|
+
if (!CONTEXT_SEGMENT_SOURCES.includes(input.source as ContextSegmentChange["source"]))
|
|
179
|
+
throw new Error("stored context change source is invalid");
|
|
180
|
+
if (!["added", "retained", "changed", "evicted", "compacted", "inactive"].includes(input.lifecycle as string))
|
|
181
|
+
throw new Error("stored context change lifecycle is invalid");
|
|
182
|
+
const deltaTokens = input.deltaTokens;
|
|
183
|
+
if (typeof deltaTokens !== "number" || !Number.isSafeInteger(deltaTokens)) throw new Error("stored context change delta is invalid");
|
|
184
|
+
return {
|
|
185
|
+
index: integer(input.index, "stored context change index", CONTEXT_SNAPSHOT_MAX_CHANGES - 1),
|
|
186
|
+
change: {
|
|
187
|
+
id: String(input.id),
|
|
188
|
+
source: input.source as ContextSegmentChange["source"],
|
|
189
|
+
lifecycle: input.lifecycle as ContextSegmentChange["lifecycle"],
|
|
190
|
+
previousTokens: integer(input.previousTokens, "stored previous context tokens"),
|
|
191
|
+
currentTokens: integer(input.currentTokens, "stored current context tokens"),
|
|
192
|
+
deltaTokens,
|
|
193
|
+
requestPosition: input.requestPosition === null ? null : integer(input.requestPosition, "stored context change position"),
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Persists snapshots using MetricStore's real atomic batch and reloads them without retaining source content. */
|
|
199
|
+
export class MetricContextSnapshotHistory implements ContextSnapshotHistory {
|
|
200
|
+
constructor(private readonly metrics: MetricStore) {}
|
|
201
|
+
|
|
202
|
+
record(value: ContextSnapshot): ContextDelta {
|
|
203
|
+
const snapshot = validateContextSnapshot(value);
|
|
204
|
+
const latest = this.latestSnapshot(snapshot.sessionId);
|
|
205
|
+
if (latest?.snapshotId === snapshot.snapshotId) {
|
|
206
|
+
const existing = this.latestDelta(snapshot.sessionId);
|
|
207
|
+
if (!existing) throw new Error("context snapshot exists without its delta");
|
|
208
|
+
return existing;
|
|
209
|
+
}
|
|
210
|
+
const delta = compareContextSnapshots(latest, snapshot);
|
|
211
|
+
this.metrics.recordBatch([
|
|
212
|
+
headerObservation(snapshot, delta),
|
|
213
|
+
...snapshot.segments.map((segment, index) => segmentObservation(snapshot, segment, index)),
|
|
214
|
+
...delta.changes.map((change, index) => changeObservation(snapshot, change, index)),
|
|
215
|
+
]);
|
|
216
|
+
return delta;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
latestSnapshot(sessionId: string): ContextSnapshot | null {
|
|
220
|
+
const header = this.latestHeader(sessionId);
|
|
221
|
+
if (!header) return null;
|
|
222
|
+
const attributes = headerAttributes(header);
|
|
223
|
+
const segments = this.rowsForSnapshot(sessionId, CONTEXT_SNAPSHOT_SEGMENT_METRIC, attributes.snapshotId, attributes.segmentCount)
|
|
224
|
+
.map(segmentFromRow)
|
|
225
|
+
.sort((left, right) => left.index - right.index)
|
|
226
|
+
.map(({ segment }) => segment);
|
|
227
|
+
if (segments.length !== attributes.segmentCount) throw new Error("stored context snapshot is incomplete");
|
|
228
|
+
return validateContextSnapshot({
|
|
229
|
+
version: 1,
|
|
230
|
+
snapshotId: attributes.snapshotId,
|
|
231
|
+
sessionId,
|
|
232
|
+
provider: attributes.provider,
|
|
233
|
+
model: attributes.model,
|
|
234
|
+
capturedAt: header.observedAt,
|
|
235
|
+
truncated: attributes.truncated,
|
|
236
|
+
segments,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
latestDelta(sessionId: string): ContextDelta | null {
|
|
241
|
+
const header = this.latestHeader(sessionId);
|
|
242
|
+
if (!header) return null;
|
|
243
|
+
const attributes = headerAttributes(header);
|
|
244
|
+
const changes = this.rowsForSnapshot(sessionId, CONTEXT_SNAPSHOT_CHANGE_METRIC, attributes.snapshotId, attributes.changeCount)
|
|
245
|
+
.map(changeFromRow)
|
|
246
|
+
.sort((left, right) => left.index - right.index)
|
|
247
|
+
.map(({ change }) => change);
|
|
248
|
+
if (changes.length !== attributes.changeCount) throw new Error("stored context delta is incomplete");
|
|
249
|
+
if (header.value === null) throw new Error("stored stable prefix is invalid");
|
|
250
|
+
return {
|
|
251
|
+
previousSnapshotId: attributes.previousSnapshotId,
|
|
252
|
+
currentSnapshotId: attributes.snapshotId,
|
|
253
|
+
capturedAt: header.observedAt,
|
|
254
|
+
truncated: attributes.truncated,
|
|
255
|
+
stablePrefixTokens: integer(header.value, "stored stable prefix"),
|
|
256
|
+
firstChangedSegment: attributes.firstChangedSegment,
|
|
257
|
+
resetReason: attributes.resetReason,
|
|
258
|
+
changes,
|
|
259
|
+
growthBySource: attributes.growthBySource,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private latestHeader(sessionId: string): StoredMetricObservation | null {
|
|
264
|
+
return (
|
|
265
|
+
this.metrics.query({
|
|
266
|
+
source: CONTEXT_SNAPSHOT_METRIC_SOURCE,
|
|
267
|
+
scope: sessionId,
|
|
268
|
+
metric: CONTEXT_SNAPSHOT_HEADER_METRIC,
|
|
269
|
+
order: "desc",
|
|
270
|
+
limit: 1,
|
|
271
|
+
})[0] ?? null
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private rowsForSnapshot(sessionId: string, metric: string, snapshotId: string, expected: number): StoredMetricObservation[] {
|
|
276
|
+
if (expected === 0) return [];
|
|
277
|
+
return this.metrics
|
|
278
|
+
.query({ source: CONTEXT_SNAPSHOT_METRIC_SOURCE, scope: sessionId, metric, order: "desc", limit: expected })
|
|
279
|
+
.filter((row) => row.attributes.snapshotId === snapshotId);
|
|
280
|
+
}
|
|
281
|
+
}
|