@tonyclaw/agent-inspector 2.0.9 → 2.0.11
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/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-Cb4RH049.js → CompareDrawer-C9OWdxHM.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CNzay4u8.js +114 -0
- package/.output/public/assets/{ReplayDialog-B3y8E7IE.js → ReplayDialog-CwE6I1Pe.js} +1 -1
- package/.output/public/assets/RequestAnatomy-DEehC3yz.js +1 -0
- package/.output/public/assets/ResponseView-CE5UsuUq.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-CLcLZ7-W.js +1 -0
- package/.output/public/assets/_sessionId-DE__tPTo.js +1 -0
- package/.output/public/assets/index-BFO4Jmw5.js +1 -0
- package/.output/public/assets/index-CDzZ7l_S.css +1 -0
- package/.output/public/assets/{main-Bww0Bc88.js → main-D1Xanzu7.js} +2 -2
- package/.output/server/{_sessionId-BnCF8Ito.mjs → _sessionId-D3IyPpws.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-jPU14EI3.mjs → CompareDrawer-BIMThqxy.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-xesEFppM.mjs → ProxyViewerContainer-BiBdKWtj.mjs} +159 -14
- package/.output/server/_ssr/{ReplayDialog-DQvW2zoW.mjs → ReplayDialog-D_wiDyM2.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-BilEphjP.mjs → RequestAnatomy-5UiMTESr.mjs} +399 -44
- package/.output/server/_ssr/{ResponseView-DB5PPcWf.mjs → ResponseView-Drk5ghmL.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-Dnrgn2AP.mjs → StreamingChunkSequence-F889rmG0.mjs} +2 -2
- package/.output/server/_ssr/{index-YPp821bH.mjs → index-CaPniq9z.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-BvuK0vQv.mjs → router-C7S8FRth.mjs} +531 -111
- package/.output/server/{_tanstack-start-manifest_v-DGw3vVOm.mjs → _tanstack-start-manifest_v-CVTkFOdT.mjs} +1 -1
- package/.output/server/index.mjs +59 -59
- package/package.json +1 -1
- package/src/components/providers/ProviderCard.tsx +59 -5
- package/src/components/providers/ProviderForm.tsx +42 -0
- package/src/components/providers/ProvidersPanel.tsx +68 -0
- package/src/components/proxy-viewer/LogEntry.tsx +6 -1
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +179 -4
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +346 -41
- package/src/lib/providerContract.ts +12 -0
- package/src/lib/providerModelMetadata.ts +327 -0
- package/src/proxy/providers.ts +112 -52
- package/src/routes/api/providers.$providerId.model-metadata.ts +134 -0
- package/src/routes/api/providers.$providerId.ts +3 -0
- package/src/routes/api/providers.ts +2 -0
- package/.output/public/assets/ProxyViewerContainer-Cmr2TANl.js +0 -114
- package/.output/public/assets/RequestAnatomy-C7BQYj95.js +0 -1
- package/.output/public/assets/ResponseView-Bylsp4WJ.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-CJH13VD3.js +0 -1
- package/.output/public/assets/_sessionId-3e8W68GL.js +0 -1
- package/.output/public/assets/index-Bo1dGS4R.css +0 -1
- package/.output/public/assets/index-Cz5aqmzE.js +0 -1
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { safeGetOwnProperty } from "../../../lib/objectUtils";
|
|
1
|
+
import { isPlainRecord, safeGetOwnProperty } from "../../../lib/objectUtils";
|
|
2
|
+
import type { ProviderConfig } from "../../../lib/providerContract";
|
|
3
|
+
import { resolveProviderContextWindow } from "../../../lib/providerModelMetadata";
|
|
4
|
+
import { countCharacters, estimateTokens } from "./tokenEstimate";
|
|
2
5
|
import type { AnatomyRole, AnatomySegment } from "./types";
|
|
3
6
|
|
|
4
7
|
export type ContextRiskLevel = "unknown" | "ok" | "watch" | "danger";
|
|
5
8
|
|
|
6
|
-
export type ContextWindowSource = "request" | "model-rule" | "unknown";
|
|
9
|
+
export type ContextWindowSource = "request" | "provider" | "model-rule" | "unknown";
|
|
10
|
+
|
|
11
|
+
export type ContextHealthLevel = "ok" | "optimizable" | "risk";
|
|
7
12
|
|
|
8
13
|
export type ContextWindow = {
|
|
9
14
|
tokens: number | null;
|
|
@@ -25,6 +30,30 @@ export type ContextDiagnostic = {
|
|
|
25
30
|
detail: string;
|
|
26
31
|
};
|
|
27
32
|
|
|
33
|
+
export type DuplicateContextSegment = {
|
|
34
|
+
role: AnatomyRole;
|
|
35
|
+
label: string;
|
|
36
|
+
path: string;
|
|
37
|
+
tokens: number;
|
|
38
|
+
characters: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type DuplicateContextGroup = {
|
|
42
|
+
key: string;
|
|
43
|
+
count: number;
|
|
44
|
+
totalTokens: number;
|
|
45
|
+
repeatedTokens: number;
|
|
46
|
+
preview: string;
|
|
47
|
+
firstLabel: string;
|
|
48
|
+
segments: DuplicateContextSegment[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ContextHealth = {
|
|
52
|
+
level: ContextHealthLevel;
|
|
53
|
+
opportunityCount: number;
|
|
54
|
+
reclaimableTokens: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
28
57
|
export type ContextIntelligence = {
|
|
29
58
|
model: string | null;
|
|
30
59
|
contextWindow: ContextWindow;
|
|
@@ -38,6 +67,8 @@ export type ContextIntelligence = {
|
|
|
38
67
|
roleUsages: RoleUsage[];
|
|
39
68
|
largestRole: RoleUsage | null;
|
|
40
69
|
diagnostics: ContextDiagnostic[];
|
|
70
|
+
duplicateGroups: DuplicateContextGroup[];
|
|
71
|
+
health: ContextHealth;
|
|
41
72
|
};
|
|
42
73
|
|
|
43
74
|
type MatchMode = "prefix" | "includes";
|
|
@@ -49,17 +80,45 @@ type ModelContextRule = {
|
|
|
49
80
|
patterns: readonly string[];
|
|
50
81
|
};
|
|
51
82
|
|
|
83
|
+
type DuplicateCandidate = {
|
|
84
|
+
role: AnatomyRole;
|
|
85
|
+
label: string;
|
|
86
|
+
path: string;
|
|
87
|
+
tokens: number;
|
|
88
|
+
characters: number;
|
|
89
|
+
text: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
52
92
|
const WATCH_USAGE_PERCENT = 0.75;
|
|
53
93
|
const DANGER_USAGE_PERCENT = 0.9;
|
|
54
94
|
const LONG_TOOL_SCHEMA_TOKENS = 8_000;
|
|
55
95
|
const LARGE_ROLE_PERCENT = 0.35;
|
|
56
96
|
const DUPLICATE_MIN_TOKENS = 40;
|
|
57
97
|
const DUPLICATE_MIN_CHARS = 160;
|
|
98
|
+
const DUPLICATE_NESTED_MAX_DEPTH = 5;
|
|
99
|
+
const DUPLICATE_NESTED_MAX_CANDIDATES_PER_SEGMENT = 8;
|
|
58
100
|
const HISTORY_BLOAT_SEGMENTS = 18;
|
|
59
101
|
const HISTORY_BLOAT_TOKENS = 60_000;
|
|
60
102
|
const HISTORY_BLOAT_PERCENT = 0.7;
|
|
61
103
|
const SYSTEM_HEAVY_TOKENS = 3_000;
|
|
62
104
|
const SYSTEM_HEAVY_PERCENT = 0.3;
|
|
105
|
+
const DUPLICATE_PREVIEW_CHARS = 160;
|
|
106
|
+
const DUPLICATE_NESTED_FIELD_NAMES = new Set([
|
|
107
|
+
"answer",
|
|
108
|
+
"body",
|
|
109
|
+
"content",
|
|
110
|
+
"data",
|
|
111
|
+
"error",
|
|
112
|
+
"message",
|
|
113
|
+
"markdown",
|
|
114
|
+
"output",
|
|
115
|
+
"response",
|
|
116
|
+
"result",
|
|
117
|
+
"stderr",
|
|
118
|
+
"stdout",
|
|
119
|
+
"text",
|
|
120
|
+
"transcript",
|
|
121
|
+
]);
|
|
63
122
|
|
|
64
123
|
const EXPLICIT_CONTEXT_WINDOW_FIELDS = [
|
|
65
124
|
"context_window",
|
|
@@ -106,6 +165,12 @@ const MODEL_CONTEXT_RULES: readonly ModelContextRule[] = [
|
|
|
106
165
|
mode: "prefix",
|
|
107
166
|
patterns: ["claude-"],
|
|
108
167
|
},
|
|
168
|
+
{
|
|
169
|
+
family: "DeepSeek V4",
|
|
170
|
+
tokens: 1_000_000,
|
|
171
|
+
mode: "prefix",
|
|
172
|
+
patterns: ["deepseek-v4-pro", "deepseek-v4-flash"],
|
|
173
|
+
},
|
|
109
174
|
{
|
|
110
175
|
family: "DeepSeek",
|
|
111
176
|
tokens: 64_000,
|
|
@@ -113,10 +178,16 @@ const MODEL_CONTEXT_RULES: readonly ModelContextRule[] = [
|
|
|
113
178
|
patterns: ["deepseek-"],
|
|
114
179
|
},
|
|
115
180
|
{
|
|
116
|
-
family: "MiniMax",
|
|
181
|
+
family: "MiniMax M3",
|
|
117
182
|
tokens: 1_000_000,
|
|
118
183
|
mode: "includes",
|
|
119
|
-
patterns: ["minimax"],
|
|
184
|
+
patterns: ["minimax-m3"],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
family: "MiniMax M2",
|
|
188
|
+
tokens: 204_800,
|
|
189
|
+
mode: "includes",
|
|
190
|
+
patterns: ["minimax-m2.7", "minimax-m2.5", "minimax-m2.1"],
|
|
120
191
|
},
|
|
121
192
|
];
|
|
122
193
|
|
|
@@ -166,7 +237,11 @@ function matchesRule(model: string, rule: ModelContextRule): boolean {
|
|
|
166
237
|
}
|
|
167
238
|
}
|
|
168
239
|
|
|
169
|
-
export function resolveContextWindow(
|
|
240
|
+
export function resolveContextWindow(
|
|
241
|
+
model: string | null,
|
|
242
|
+
parsed: unknown,
|
|
243
|
+
providers: readonly ProviderConfig[] = [],
|
|
244
|
+
): ContextWindow {
|
|
170
245
|
const explicitWindow = readPositiveIntegerField(parsed, EXPLICIT_CONTEXT_WINDOW_FIELDS);
|
|
171
246
|
if (explicitWindow !== null) {
|
|
172
247
|
return {
|
|
@@ -176,6 +251,15 @@ export function resolveContextWindow(model: string | null, parsed: unknown): Con
|
|
|
176
251
|
};
|
|
177
252
|
}
|
|
178
253
|
|
|
254
|
+
const providerWindow = resolveProviderContextWindow(model, providers);
|
|
255
|
+
if (providerWindow !== null) {
|
|
256
|
+
return {
|
|
257
|
+
tokens: providerWindow.tokens,
|
|
258
|
+
label: `${providerWindow.providerName} config`,
|
|
259
|
+
source: "provider",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
179
263
|
if (model === null) {
|
|
180
264
|
return {
|
|
181
265
|
tokens: null,
|
|
@@ -184,12 +268,12 @@ export function resolveContextWindow(model: string | null, parsed: unknown): Con
|
|
|
184
268
|
};
|
|
185
269
|
}
|
|
186
270
|
|
|
187
|
-
const normalized = model.trim().toLowerCase();
|
|
271
|
+
const normalized = model.trim().toLowerCase().replace(/\s+/g, "-");
|
|
188
272
|
for (const rule of MODEL_CONTEXT_RULES) {
|
|
189
273
|
if (matchesRule(normalized, rule)) {
|
|
190
274
|
return {
|
|
191
275
|
tokens: rule.tokens,
|
|
192
|
-
label: rule.family
|
|
276
|
+
label: `${rule.family} local rule`,
|
|
193
277
|
source: "model-rule",
|
|
194
278
|
};
|
|
195
279
|
}
|
|
@@ -241,51 +325,230 @@ function normalizeDuplicateText(text: string): string {
|
|
|
241
325
|
return text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
242
326
|
}
|
|
243
327
|
|
|
244
|
-
function
|
|
328
|
+
function duplicatePreview(text: string): string {
|
|
329
|
+
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
330
|
+
if (singleLine.length <= DUPLICATE_PREVIEW_CHARS) return singleLine;
|
|
331
|
+
return `${singleLine.slice(0, DUPLICATE_PREVIEW_CHARS - 3)}...`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function duplicateSegmentRef(candidate: DuplicateCandidate): DuplicateContextSegment {
|
|
335
|
+
return {
|
|
336
|
+
role: candidate.role,
|
|
337
|
+
label: candidate.label,
|
|
338
|
+
path: candidate.path,
|
|
339
|
+
tokens: candidate.tokens,
|
|
340
|
+
characters: candidate.characters,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function duplicateCandidateFromSegment(segment: AnatomySegment): DuplicateCandidate {
|
|
345
|
+
return {
|
|
346
|
+
role: segment.role,
|
|
347
|
+
label: segment.label,
|
|
348
|
+
path: segment.path,
|
|
349
|
+
tokens: segment.size,
|
|
350
|
+
characters: segment.characters,
|
|
351
|
+
text: segment.text,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function jsonLikeText(text: string): boolean {
|
|
356
|
+
const trimmed = text.trim();
|
|
357
|
+
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function parseJsonLikeText(text: string): unknown | null {
|
|
361
|
+
if (!jsonLikeText(text)) return null;
|
|
362
|
+
try {
|
|
363
|
+
const parsed: unknown = JSON.parse(text);
|
|
364
|
+
return parsed;
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function normalizePathFieldName(field: string): string {
|
|
371
|
+
return field.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function lastNamedPathField(path: readonly string[]): string | null {
|
|
375
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
376
|
+
const field = path[index] ?? "";
|
|
377
|
+
if (field.startsWith("[") && field.endsWith("]")) continue;
|
|
378
|
+
return field;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function nestedDuplicateFieldPath(path: readonly string[]): boolean {
|
|
384
|
+
const field = lastNamedPathField(path);
|
|
385
|
+
if (field === null) return false;
|
|
386
|
+
return DUPLICATE_NESTED_FIELD_NAMES.has(normalizePathFieldName(field));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function formatNestedDuplicatePath(path: readonly string[]): string {
|
|
390
|
+
return path
|
|
391
|
+
.map((part) => (part.startsWith("[") ? part : `.${part}`))
|
|
392
|
+
.join("")
|
|
393
|
+
.replace(/^\./, "");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function duplicateCandidateFromNestedText(
|
|
397
|
+
segment: AnatomySegment,
|
|
398
|
+
text: string,
|
|
399
|
+
path: readonly string[],
|
|
400
|
+
): DuplicateCandidate | null {
|
|
401
|
+
const characters = countCharacters(text);
|
|
402
|
+
if (characters < DUPLICATE_MIN_CHARS) return null;
|
|
403
|
+
const tokens = estimateTokens(text);
|
|
404
|
+
if (tokens < DUPLICATE_MIN_TOKENS) return null;
|
|
405
|
+
return {
|
|
406
|
+
role: segment.role,
|
|
407
|
+
label: `${segment.label} ${formatNestedDuplicatePath(path)}`,
|
|
408
|
+
path: segment.path,
|
|
409
|
+
tokens,
|
|
410
|
+
characters,
|
|
411
|
+
text,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function collectNestedDuplicateCandidates({
|
|
416
|
+
value,
|
|
417
|
+
segment,
|
|
418
|
+
path,
|
|
419
|
+
depth,
|
|
420
|
+
candidates,
|
|
421
|
+
}: {
|
|
422
|
+
value: unknown;
|
|
423
|
+
segment: AnatomySegment;
|
|
424
|
+
path: readonly string[];
|
|
425
|
+
depth: number;
|
|
426
|
+
candidates: DuplicateCandidate[];
|
|
427
|
+
}): void {
|
|
428
|
+
if (candidates.length >= DUPLICATE_NESTED_MAX_CANDIDATES_PER_SEGMENT) return;
|
|
429
|
+
if (depth > DUPLICATE_NESTED_MAX_DEPTH) return;
|
|
430
|
+
|
|
431
|
+
if (typeof value === "string") {
|
|
432
|
+
if (!nestedDuplicateFieldPath(path)) return;
|
|
433
|
+
const candidate = duplicateCandidateFromNestedText(segment, value, path);
|
|
434
|
+
if (candidate !== null) candidates.push(candidate);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (Array.isArray(value)) {
|
|
439
|
+
value.forEach((item, index) =>
|
|
440
|
+
collectNestedDuplicateCandidates({
|
|
441
|
+
value: item,
|
|
442
|
+
segment,
|
|
443
|
+
path: [...path, `[${String(index)}]`],
|
|
444
|
+
depth: depth + 1,
|
|
445
|
+
candidates,
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (!isPlainRecord(value)) return;
|
|
452
|
+
Object.entries(value).forEach(([key, child]) =>
|
|
453
|
+
collectNestedDuplicateCandidates({
|
|
454
|
+
value: child,
|
|
455
|
+
segment,
|
|
456
|
+
path: [...path, key],
|
|
457
|
+
depth: depth + 1,
|
|
458
|
+
candidates,
|
|
459
|
+
}),
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function duplicateCandidatesForSegment(segment: AnatomySegment): DuplicateCandidate[] {
|
|
464
|
+
const segmentCandidate = duplicateCandidateFromSegment(segment);
|
|
465
|
+
const parsed = parseJsonLikeText(segment.text);
|
|
466
|
+
if (parsed === null) return [segmentCandidate];
|
|
467
|
+
|
|
468
|
+
const candidates: DuplicateCandidate[] = [];
|
|
469
|
+
collectNestedDuplicateCandidates({
|
|
470
|
+
value: parsed,
|
|
471
|
+
segment,
|
|
472
|
+
path: [],
|
|
473
|
+
depth: 0,
|
|
474
|
+
candidates,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const normalizedSegment = normalizeDuplicateText(segment.text);
|
|
478
|
+
const seen = new Set([normalizedSegment]);
|
|
479
|
+
const uniqueNested = candidates.filter((candidate) => {
|
|
480
|
+
const normalized = normalizeDuplicateText(candidate.text);
|
|
481
|
+
if (normalized === "") return false;
|
|
482
|
+
if (seen.has(normalized)) return false;
|
|
483
|
+
seen.add(normalized);
|
|
484
|
+
return true;
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
return [segmentCandidate, ...uniqueNested];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function duplicateContextGroups(segments: readonly AnatomySegment[]): DuplicateContextGroup[] {
|
|
245
491
|
type DuplicateBucket = {
|
|
246
|
-
count: number;
|
|
247
492
|
tokens: number;
|
|
248
|
-
|
|
249
|
-
label: string;
|
|
493
|
+
candidates: DuplicateCandidate[];
|
|
250
494
|
};
|
|
251
495
|
|
|
252
496
|
const buckets = new Map<string, DuplicateBucket>();
|
|
253
497
|
for (const segment of segments) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
498
|
+
const candidates = duplicateCandidatesForSegment(segment);
|
|
499
|
+
for (const candidate of candidates) {
|
|
500
|
+
if (candidate.tokens < DUPLICATE_MIN_TOKENS) continue;
|
|
501
|
+
if (candidate.characters < DUPLICATE_MIN_CHARS) continue;
|
|
502
|
+
const normalized = normalizeDuplicateText(candidate.text);
|
|
503
|
+
if (normalized === "") continue;
|
|
504
|
+
const existing = buckets.get(normalized);
|
|
505
|
+
if (existing === undefined) {
|
|
506
|
+
buckets.set(normalized, {
|
|
507
|
+
tokens: candidate.tokens,
|
|
508
|
+
candidates: [candidate],
|
|
509
|
+
});
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
260
512
|
buckets.set(normalized, {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
firstTokens: segment.size,
|
|
264
|
-
label: segment.label,
|
|
513
|
+
tokens: existing.tokens + candidate.tokens,
|
|
514
|
+
candidates: [...existing.candidates, candidate],
|
|
265
515
|
});
|
|
266
|
-
continue;
|
|
267
516
|
}
|
|
268
|
-
buckets.set(normalized, {
|
|
269
|
-
count: existing.count + 1,
|
|
270
|
-
tokens: existing.tokens + segment.size,
|
|
271
|
-
firstTokens: existing.firstTokens,
|
|
272
|
-
label: existing.label,
|
|
273
|
-
});
|
|
274
517
|
}
|
|
275
518
|
|
|
276
|
-
|
|
277
|
-
let repeatedSegments = 0;
|
|
278
|
-
let repeatedTokens = 0;
|
|
279
|
-
let largest: DuplicateBucket | null = null;
|
|
519
|
+
const groups: DuplicateContextGroup[] = [];
|
|
280
520
|
for (const bucket of buckets.values()) {
|
|
281
|
-
if (bucket.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
repeatedTokens
|
|
285
|
-
|
|
521
|
+
if (bucket.candidates.length < 2) continue;
|
|
522
|
+
const firstCandidate = bucket.candidates[0] ?? null;
|
|
523
|
+
if (firstCandidate === null) continue;
|
|
524
|
+
const repeatedTokens = bucket.tokens - firstCandidate.tokens;
|
|
525
|
+
groups.push({
|
|
526
|
+
key: `${firstCandidate.path}:${firstCandidate.label}:${String(
|
|
527
|
+
bucket.candidates.length,
|
|
528
|
+
)}:${String(bucket.tokens)}`,
|
|
529
|
+
count: bucket.candidates.length,
|
|
530
|
+
totalTokens: bucket.tokens,
|
|
531
|
+
repeatedTokens,
|
|
532
|
+
preview: duplicatePreview(firstCandidate.text),
|
|
533
|
+
firstLabel: firstCandidate.label,
|
|
534
|
+
segments: bucket.candidates.map(duplicateSegmentRef),
|
|
535
|
+
});
|
|
286
536
|
}
|
|
287
537
|
|
|
288
|
-
|
|
538
|
+
return groups.sort(
|
|
539
|
+
(left, right) =>
|
|
540
|
+
right.repeatedTokens - left.repeatedTokens || right.totalTokens - left.totalTokens,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function duplicateDiagnostic(
|
|
545
|
+
duplicateGroups: readonly DuplicateContextGroup[],
|
|
546
|
+
): ContextDiagnostic | null {
|
|
547
|
+
if (duplicateGroups.length === 0) return null;
|
|
548
|
+
|
|
549
|
+
const repeatedGroups = duplicateGroups.length;
|
|
550
|
+
const repeatedSegments = duplicateGroups.reduce((sum, group) => sum + group.count, 0);
|
|
551
|
+
const repeatedTokens = duplicateGroups.reduce((sum, group) => sum + group.repeatedTokens, 0);
|
|
289
552
|
|
|
290
553
|
return {
|
|
291
554
|
kind: "duplicate",
|
|
@@ -297,6 +560,10 @@ function duplicateDiagnostic(segments: readonly AnatomySegment[]): ContextDiagno
|
|
|
297
560
|
};
|
|
298
561
|
}
|
|
299
562
|
|
|
563
|
+
function duplicateReclaimableTokens(duplicateGroups: readonly DuplicateContextGroup[]): number {
|
|
564
|
+
return duplicateGroups.reduce((sum, group) => sum + group.repeatedTokens, 0);
|
|
565
|
+
}
|
|
566
|
+
|
|
300
567
|
function diagnosticsForRolePressure(
|
|
301
568
|
roleUsages: readonly RoleUsage[],
|
|
302
569
|
total: number,
|
|
@@ -352,22 +619,51 @@ function diagnosticsForRolePressure(
|
|
|
352
619
|
return diagnostics;
|
|
353
620
|
}
|
|
354
621
|
|
|
622
|
+
function contextHealth({
|
|
623
|
+
risk,
|
|
624
|
+
contextWindow,
|
|
625
|
+
diagnostics,
|
|
626
|
+
duplicateGroups,
|
|
627
|
+
}: {
|
|
628
|
+
risk: ContextRiskLevel;
|
|
629
|
+
contextWindow: ContextWindow;
|
|
630
|
+
diagnostics: readonly ContextDiagnostic[];
|
|
631
|
+
duplicateGroups: readonly DuplicateContextGroup[];
|
|
632
|
+
}): ContextHealth {
|
|
633
|
+
const diagnosticKinds = new Set(diagnostics.map((diagnostic) => diagnostic.kind));
|
|
634
|
+
let opportunityCount = diagnosticKinds.size;
|
|
635
|
+
if (risk === "watch" || risk === "danger") opportunityCount += 1;
|
|
636
|
+
if (contextWindow.source === "unknown") opportunityCount += 1;
|
|
637
|
+
|
|
638
|
+
const hasDangerDiagnostic = diagnostics.some((diagnostic) => diagnostic.severity === "danger");
|
|
639
|
+
const level: ContextHealthLevel =
|
|
640
|
+
risk === "danger" || hasDangerDiagnostic ? "risk" : opportunityCount > 0 ? "optimizable" : "ok";
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
level,
|
|
644
|
+
opportunityCount,
|
|
645
|
+
reclaimableTokens: duplicateReclaimableTokens(duplicateGroups),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
355
649
|
export function analyzeContextIntelligence({
|
|
356
650
|
segments,
|
|
357
651
|
inputTokens,
|
|
358
652
|
parsed,
|
|
359
653
|
model,
|
|
654
|
+
providers = [],
|
|
360
655
|
}: {
|
|
361
656
|
segments: readonly AnatomySegment[];
|
|
362
657
|
inputTokens: number | null;
|
|
363
658
|
parsed: unknown;
|
|
364
659
|
model: string | null;
|
|
660
|
+
providers?: readonly ProviderConfig[];
|
|
365
661
|
}): ContextIntelligence {
|
|
366
662
|
const totalEstimatedTokens = segments.reduce((sum, segment) => sum + segment.size, 0);
|
|
367
663
|
const requestModel = model ?? readRequestModel(parsed);
|
|
368
664
|
const estimatedInputTokens = inputTokens ?? totalEstimatedTokens;
|
|
369
665
|
const outputReserveTokens = readPositiveIntegerField(parsed, OUTPUT_RESERVE_FIELDS);
|
|
370
|
-
const contextWindow = resolveContextWindow(requestModel, parsed);
|
|
666
|
+
const contextWindow = resolveContextWindow(requestModel, parsed, providers);
|
|
371
667
|
const reservedTokens = outputReserveTokens ?? 0;
|
|
372
668
|
const windowUsedTokens =
|
|
373
669
|
contextWindow.tokens === null ? null : estimatedInputTokens + reservedTokens;
|
|
@@ -384,13 +680,15 @@ export function analyzeContextIntelligence({
|
|
|
384
680
|
const roleUsages = buildRoleUsages(segments, totalEstimatedTokens);
|
|
385
681
|
const largestRole = roleUsages[0] ?? null;
|
|
386
682
|
const messageSegmentCount = segments.filter((segment) => segment.role !== "tools").length;
|
|
387
|
-
const
|
|
683
|
+
const duplicateGroups = duplicateContextGroups(segments);
|
|
684
|
+
const duplicate = duplicateDiagnostic(duplicateGroups);
|
|
388
685
|
const diagnostics = diagnosticsForRolePressure(
|
|
389
686
|
roleUsages,
|
|
390
687
|
totalEstimatedTokens,
|
|
391
688
|
messageSegmentCount,
|
|
392
689
|
);
|
|
393
690
|
if (duplicate !== null) diagnostics.unshift(duplicate);
|
|
691
|
+
const risk = riskLevel(usagePercent);
|
|
394
692
|
|
|
395
693
|
return {
|
|
396
694
|
model: requestModel,
|
|
@@ -401,10 +699,17 @@ export function analyzeContextIntelligence({
|
|
|
401
699
|
remainingInputTokens,
|
|
402
700
|
remainingAfterReserveTokens,
|
|
403
701
|
usagePercent,
|
|
404
|
-
riskLevel:
|
|
702
|
+
riskLevel: risk,
|
|
405
703
|
roleUsages,
|
|
406
704
|
largestRole,
|
|
407
705
|
diagnostics,
|
|
706
|
+
duplicateGroups,
|
|
707
|
+
health: contextHealth({
|
|
708
|
+
risk,
|
|
709
|
+
contextWindow,
|
|
710
|
+
diagnostics,
|
|
711
|
+
duplicateGroups,
|
|
712
|
+
}),
|
|
408
713
|
};
|
|
409
714
|
}
|
|
410
715
|
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
export const ProviderModelMetadataSchema = z.object({
|
|
4
|
+
model: z.string().min(1),
|
|
5
|
+
contextWindow: z.number().int().positive().optional(),
|
|
6
|
+
outputLimit: z.number().int().positive().optional(),
|
|
7
|
+
source: z.enum(["manual", "registry"]).optional(),
|
|
8
|
+
sourceUrl: z.string().optional(),
|
|
9
|
+
updatedAt: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
3
12
|
/**
|
|
4
13
|
* Persisted provider shape shared by the server store and browser API clients.
|
|
5
14
|
*
|
|
@@ -18,9 +27,12 @@ export const ProviderConfigSchema = z.object({
|
|
|
18
27
|
openaiBaseUrl: z.string().optional(),
|
|
19
28
|
authHeader: z.enum(["bearer", "x-api-key"]).optional().default("bearer"),
|
|
20
29
|
apiDocsUrl: z.string().optional(),
|
|
30
|
+
modelMetadataUrl: z.string().optional(),
|
|
31
|
+
modelMetadata: z.array(ProviderModelMetadataSchema).optional(),
|
|
21
32
|
source: z.enum(["company", "personal"]).optional(),
|
|
22
33
|
createdAt: z.string(),
|
|
23
34
|
updatedAt: z.string(),
|
|
24
35
|
});
|
|
25
36
|
|
|
37
|
+
export type ProviderModelMetadata = z.infer<typeof ProviderModelMetadataSchema>;
|
|
26
38
|
export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
|