@tonyclaw/agent-inspector 2.0.8 → 2.0.10
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-Djb4XFZS.js → CompareDrawer-BWMipEjS.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DIAgurux.js +114 -0
- package/.output/public/assets/{ReplayDialog-DBPMIs2I.js → ReplayDialog-DaSzZsvM.js} +1 -1
- package/.output/public/assets/RequestAnatomy-BfQmPHCd.js +1 -0
- package/.output/public/assets/ResponseView-CblEWDMx.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-B4atnL99.js +1 -0
- package/.output/public/assets/_sessionId-DUzYnZXV.js +1 -0
- package/.output/public/assets/index-CxhRsekH.css +1 -0
- package/.output/public/assets/index-jagRsQaV.js +1 -0
- package/.output/public/assets/{main-CwhqfJqM.js → main-DE6Y4-wV.js} +2 -2
- package/.output/server/{_sessionId-DzBy9gLa.mjs → _sessionId-61HO9rUI.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-C5NBvE2e.mjs → CompareDrawer-CSPqneYm.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-DiZjOTQT.mjs → ProxyViewerContainer-DrSMa5O7.mjs} +159 -14
- package/.output/server/_ssr/{ReplayDialog-EpvTwFvp.mjs → ReplayDialog-Dn3naENv.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-DaQjhixp.mjs → RequestAnatomy-D1X3f3AX.mjs} +336 -42
- package/.output/server/_ssr/{ResponseView-D8tGHCGY.mjs → ResponseView-CwXAWOoE.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-DGQnb1QE.mjs → StreamingChunkSequence-DRSR2FfN.mjs} +2 -2
- package/.output/server/_ssr/{index-N25hqa7t.mjs → index-D4zn5CAg.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-Bf0m6F0G.mjs → router-CSRaa_tu.mjs} +531 -111
- package/.output/server/{_tanstack-start-manifest_v-DlAyJ5DB.mjs → _tanstack-start-manifest_v-CfvVUYYj.mjs} +1 -1
- package/.output/server/index.mjs +60 -60
- package/bin/agent-inspector.js +2 -0
- package/package.json +3 -2
- 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 +134 -3
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +298 -40
- 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-CWdkPZsJ.js +0 -114
- package/.output/public/assets/RequestAnatomy-FBfRNQN7.js +0 -1
- package/.output/public/assets/ResponseView-Db4pJL7U.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-BH175O6R.js +0 -1
- package/.output/public/assets/_sessionId-3ipVoQtW.js +0 -1
- package/.output/public/assets/index-Bo1dGS4R.css +0 -1
- package/.output/public/assets/index-CJgaCJo8.js +0 -1
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Info } from "lucide-react";
|
|
2
2
|
import { type JSX, useMemo, useState } from "react";
|
|
3
3
|
import { cn, formatTokens } from "../../../lib/utils";
|
|
4
|
+
import { useProviders } from "../../../lib/useProviders";
|
|
4
5
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ui/tooltip";
|
|
5
6
|
import {
|
|
6
7
|
analyzeContextIntelligence,
|
|
7
8
|
CONTEXT_USAGE_THRESHOLDS,
|
|
8
9
|
type ContextDiagnostic,
|
|
10
|
+
type DuplicateContextGroup,
|
|
11
|
+
type DuplicateContextSegment,
|
|
9
12
|
type ContextIntelligence,
|
|
10
13
|
type ContextRiskLevel,
|
|
11
14
|
} from "./contextIntelligence";
|
|
@@ -29,6 +32,8 @@ type ContextViewMode = "role" | "segment";
|
|
|
29
32
|
|
|
30
33
|
const DIVERGENCE_AMBER_THRESHOLD = 0.25;
|
|
31
34
|
const TOP_CONTRIBUTOR_COUNT = 5;
|
|
35
|
+
const DUPLICATE_GROUP_DISPLAY_COUNT = 4;
|
|
36
|
+
const DUPLICATE_SEGMENT_DISPLAY_COUNT = 6;
|
|
32
37
|
const ROLE_ORDER: AnatomyRole[] = ["system", "user", "assistant", "tool", "tools"];
|
|
33
38
|
|
|
34
39
|
const ROLE_DESCRIPTIONS: Record<AnatomyRole, string> = {
|
|
@@ -85,6 +90,8 @@ function formatContextSource(intelligence: ContextIntelligence): string {
|
|
|
85
90
|
switch (intelligence.contextWindow.source) {
|
|
86
91
|
case "request":
|
|
87
92
|
return intelligence.contextWindow.label;
|
|
93
|
+
case "provider":
|
|
94
|
+
return intelligence.contextWindow.label;
|
|
88
95
|
case "model-rule":
|
|
89
96
|
return intelligence.contextWindow.label;
|
|
90
97
|
case "unknown":
|
|
@@ -115,6 +122,10 @@ function ContextIntelligenceStrip({
|
|
|
115
122
|
? "No output reserve"
|
|
116
123
|
: `Output reserve ${formatTokens(intelligence.outputReserveTokens)}`;
|
|
117
124
|
const largestRole = intelligence.largestRole;
|
|
125
|
+
const visibleDiagnostics =
|
|
126
|
+
intelligence.duplicateGroups.length === 0
|
|
127
|
+
? intelligence.diagnostics
|
|
128
|
+
: intelligence.diagnostics.filter((diagnostic) => diagnostic.kind !== "duplicate");
|
|
118
129
|
|
|
119
130
|
return (
|
|
120
131
|
<div className="space-y-2">
|
|
@@ -199,9 +210,9 @@ function ContextIntelligenceStrip({
|
|
|
199
210
|
</div>
|
|
200
211
|
)}
|
|
201
212
|
|
|
202
|
-
{
|
|
213
|
+
{visibleDiagnostics.length > 0 && (
|
|
203
214
|
<div className="grid gap-1.5 sm:grid-cols-2">
|
|
204
|
-
{
|
|
215
|
+
{visibleDiagnostics.map((diagnostic) => (
|
|
205
216
|
<div
|
|
206
217
|
key={`${diagnostic.kind}-${diagnostic.title}`}
|
|
207
218
|
className={cn("rounded border px-2 py-1.5 text-xs", diagnosticTone(diagnostic))}
|
|
@@ -216,6 +227,116 @@ function ContextIntelligenceStrip({
|
|
|
216
227
|
);
|
|
217
228
|
}
|
|
218
229
|
|
|
230
|
+
function resolveDuplicateSegment(
|
|
231
|
+
duplicateSegment: DuplicateContextSegment,
|
|
232
|
+
segments: readonly AnatomySegment[],
|
|
233
|
+
): AnatomySegment | null {
|
|
234
|
+
return segments.find((segment) => segment.path === duplicateSegment.path) ?? null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function DuplicateContentPanel({
|
|
238
|
+
groups,
|
|
239
|
+
segments,
|
|
240
|
+
onSegmentActivate,
|
|
241
|
+
}: {
|
|
242
|
+
groups: readonly DuplicateContextGroup[];
|
|
243
|
+
segments: readonly AnatomySegment[];
|
|
244
|
+
onSegmentActivate?: (segment: AnatomySegment) => void;
|
|
245
|
+
}): JSX.Element | null {
|
|
246
|
+
if (groups.length === 0) return null;
|
|
247
|
+
|
|
248
|
+
const visibleGroups = groups.slice(0, DUPLICATE_GROUP_DISPLAY_COUNT);
|
|
249
|
+
const hiddenGroupCount = groups.length - visibleGroups.length;
|
|
250
|
+
const totalRepeatedTokens = groups.reduce((sum, group) => sum + group.repeatedTokens, 0);
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<section className="space-y-2 rounded border border-border/70 bg-muted/15 p-2">
|
|
254
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
255
|
+
<div className="text-xs font-medium text-foreground">Duplicate Content</div>
|
|
256
|
+
<div className="font-mono text-[11px] text-muted-foreground">
|
|
257
|
+
{String(groups.length)} group{groups.length === 1 ? "" : "s"} | ~
|
|
258
|
+
{formatTokens(totalRepeatedTokens)} repeated
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div className="space-y-2">
|
|
263
|
+
{visibleGroups.map((group, groupIndex) => {
|
|
264
|
+
const visibleSegments = group.segments.slice(0, DUPLICATE_SEGMENT_DISPLAY_COUNT);
|
|
265
|
+
const hiddenSegmentCount = group.segments.length - visibleSegments.length;
|
|
266
|
+
return (
|
|
267
|
+
<div
|
|
268
|
+
key={group.key}
|
|
269
|
+
className="space-y-1.5 border-t border-border/50 pt-2 first:border-t-0 first:pt-0"
|
|
270
|
+
>
|
|
271
|
+
<div className="flex min-w-0 items-center gap-2 text-xs">
|
|
272
|
+
<span className="w-5 shrink-0 text-right font-mono text-muted-foreground/70">
|
|
273
|
+
{String(groupIndex + 1)}
|
|
274
|
+
</span>
|
|
275
|
+
<span className="min-w-0 flex-1 truncate text-foreground">{group.firstLabel}</span>
|
|
276
|
+
<span className="shrink-0 font-mono text-muted-foreground">
|
|
277
|
+
x{String(group.count)} | ~{formatTokens(group.repeatedTokens)} saved
|
|
278
|
+
</span>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{group.preview.length > 0 && (
|
|
282
|
+
<div className="ml-7 line-clamp-2 break-words rounded bg-background/40 px-2 py-1 font-mono text-[11px] leading-4 text-muted-foreground">
|
|
283
|
+
{group.preview}
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
<div className="ml-7 flex flex-wrap gap-1">
|
|
288
|
+
{visibleSegments.map((duplicateSegment, segmentIndex) => {
|
|
289
|
+
const target = resolveDuplicateSegment(duplicateSegment, segments);
|
|
290
|
+
const activate =
|
|
291
|
+
target === null || onSegmentActivate === undefined
|
|
292
|
+
? undefined
|
|
293
|
+
: onSegmentActivate;
|
|
294
|
+
const label = `${duplicateSegment.label} ~${formatTokens(
|
|
295
|
+
duplicateSegment.tokens,
|
|
296
|
+
)}`;
|
|
297
|
+
if (activate === undefined || target === null) {
|
|
298
|
+
return (
|
|
299
|
+
<span
|
|
300
|
+
key={`${duplicateSegment.path}-${segmentIndex}`}
|
|
301
|
+
className="inline-flex h-6 max-w-40 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground"
|
|
302
|
+
title={label}
|
|
303
|
+
>
|
|
304
|
+
<span className="truncate">{label}</span>
|
|
305
|
+
</span>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return (
|
|
309
|
+
<button
|
|
310
|
+
key={`${duplicateSegment.path}-${segmentIndex}`}
|
|
311
|
+
type="button"
|
|
312
|
+
onClick={() => activate(target)}
|
|
313
|
+
className="inline-flex h-6 max-w-40 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground hover:border-border hover:bg-muted/40 hover:text-foreground"
|
|
314
|
+
title="Jump to this duplicate block"
|
|
315
|
+
>
|
|
316
|
+
<span className="truncate">{label}</span>
|
|
317
|
+
</button>
|
|
318
|
+
);
|
|
319
|
+
})}
|
|
320
|
+
{hiddenSegmentCount > 0 && (
|
|
321
|
+
<span className="inline-flex h-6 items-center rounded border border-border/40 px-1.5 font-mono text-[11px] text-muted-foreground/70">
|
|
322
|
+
+{String(hiddenSegmentCount)}
|
|
323
|
+
</span>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
})}
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{hiddenGroupCount > 0 && (
|
|
332
|
+
<div className="border-t border-border/50 pt-2 font-mono text-[11px] text-muted-foreground">
|
|
333
|
+
+{String(hiddenGroupCount)} more duplicate group{hiddenGroupCount === 1 ? "" : "s"}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
</section>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
219
340
|
function aggregateByRole(segments: AnatomySegment[]): AnatomySegment[] {
|
|
220
341
|
const result: AnatomySegment[] = [];
|
|
221
342
|
for (const role of ROLE_ORDER) {
|
|
@@ -254,6 +375,7 @@ export function RequestAnatomy({
|
|
|
254
375
|
onSegmentActivate,
|
|
255
376
|
segments: providedSegments,
|
|
256
377
|
}: RequestAnatomyProps): JSX.Element | null {
|
|
378
|
+
const { providers } = useProviders();
|
|
257
379
|
const [viewMode, setViewMode] = useState<ContextViewMode>("role");
|
|
258
380
|
const segments = useMemo(() => providedSegments ?? null, [providedSegments]);
|
|
259
381
|
const total = useMemo(
|
|
@@ -277,8 +399,9 @@ export function RequestAnatomy({
|
|
|
277
399
|
inputTokens,
|
|
278
400
|
parsed,
|
|
279
401
|
model,
|
|
402
|
+
providers,
|
|
280
403
|
}),
|
|
281
|
-
[inputTokens, model, parsed, segments],
|
|
404
|
+
[inputTokens, model, parsed, providers, segments],
|
|
282
405
|
);
|
|
283
406
|
|
|
284
407
|
const showDivergenceWarning = useMemo(() => {
|
|
@@ -368,6 +491,14 @@ export function RequestAnatomy({
|
|
|
368
491
|
|
|
369
492
|
{intelligence !== null && <ContextIntelligenceStrip intelligence={intelligence} />}
|
|
370
493
|
|
|
494
|
+
{intelligence !== null && (
|
|
495
|
+
<DuplicateContentPanel
|
|
496
|
+
groups={intelligence.duplicateGroups}
|
|
497
|
+
segments={segments}
|
|
498
|
+
onSegmentActivate={onSegmentActivate}
|
|
499
|
+
/>
|
|
500
|
+
)}
|
|
501
|
+
|
|
371
502
|
<SegmentBar
|
|
372
503
|
segments={displayedSegments}
|
|
373
504
|
totalTokens={displayedTotal}
|
|
@@ -1,9 +1,12 @@
|
|
|
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";
|
|
7
10
|
|
|
8
11
|
export type ContextWindow = {
|
|
9
12
|
tokens: number | null;
|
|
@@ -25,6 +28,24 @@ export type ContextDiagnostic = {
|
|
|
25
28
|
detail: string;
|
|
26
29
|
};
|
|
27
30
|
|
|
31
|
+
export type DuplicateContextSegment = {
|
|
32
|
+
role: AnatomyRole;
|
|
33
|
+
label: string;
|
|
34
|
+
path: string;
|
|
35
|
+
tokens: number;
|
|
36
|
+
characters: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type DuplicateContextGroup = {
|
|
40
|
+
key: string;
|
|
41
|
+
count: number;
|
|
42
|
+
totalTokens: number;
|
|
43
|
+
repeatedTokens: number;
|
|
44
|
+
preview: string;
|
|
45
|
+
firstLabel: string;
|
|
46
|
+
segments: DuplicateContextSegment[];
|
|
47
|
+
};
|
|
48
|
+
|
|
28
49
|
export type ContextIntelligence = {
|
|
29
50
|
model: string | null;
|
|
30
51
|
contextWindow: ContextWindow;
|
|
@@ -38,6 +59,7 @@ export type ContextIntelligence = {
|
|
|
38
59
|
roleUsages: RoleUsage[];
|
|
39
60
|
largestRole: RoleUsage | null;
|
|
40
61
|
diagnostics: ContextDiagnostic[];
|
|
62
|
+
duplicateGroups: DuplicateContextGroup[];
|
|
41
63
|
};
|
|
42
64
|
|
|
43
65
|
type MatchMode = "prefix" | "includes";
|
|
@@ -49,17 +71,45 @@ type ModelContextRule = {
|
|
|
49
71
|
patterns: readonly string[];
|
|
50
72
|
};
|
|
51
73
|
|
|
74
|
+
type DuplicateCandidate = {
|
|
75
|
+
role: AnatomyRole;
|
|
76
|
+
label: string;
|
|
77
|
+
path: string;
|
|
78
|
+
tokens: number;
|
|
79
|
+
characters: number;
|
|
80
|
+
text: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
52
83
|
const WATCH_USAGE_PERCENT = 0.75;
|
|
53
84
|
const DANGER_USAGE_PERCENT = 0.9;
|
|
54
85
|
const LONG_TOOL_SCHEMA_TOKENS = 8_000;
|
|
55
86
|
const LARGE_ROLE_PERCENT = 0.35;
|
|
56
87
|
const DUPLICATE_MIN_TOKENS = 40;
|
|
57
88
|
const DUPLICATE_MIN_CHARS = 160;
|
|
89
|
+
const DUPLICATE_NESTED_MAX_DEPTH = 5;
|
|
90
|
+
const DUPLICATE_NESTED_MAX_CANDIDATES_PER_SEGMENT = 8;
|
|
58
91
|
const HISTORY_BLOAT_SEGMENTS = 18;
|
|
59
92
|
const HISTORY_BLOAT_TOKENS = 60_000;
|
|
60
93
|
const HISTORY_BLOAT_PERCENT = 0.7;
|
|
61
94
|
const SYSTEM_HEAVY_TOKENS = 3_000;
|
|
62
95
|
const SYSTEM_HEAVY_PERCENT = 0.3;
|
|
96
|
+
const DUPLICATE_PREVIEW_CHARS = 160;
|
|
97
|
+
const DUPLICATE_NESTED_FIELD_NAMES = new Set([
|
|
98
|
+
"answer",
|
|
99
|
+
"body",
|
|
100
|
+
"content",
|
|
101
|
+
"data",
|
|
102
|
+
"error",
|
|
103
|
+
"message",
|
|
104
|
+
"markdown",
|
|
105
|
+
"output",
|
|
106
|
+
"response",
|
|
107
|
+
"result",
|
|
108
|
+
"stderr",
|
|
109
|
+
"stdout",
|
|
110
|
+
"text",
|
|
111
|
+
"transcript",
|
|
112
|
+
]);
|
|
63
113
|
|
|
64
114
|
const EXPLICIT_CONTEXT_WINDOW_FIELDS = [
|
|
65
115
|
"context_window",
|
|
@@ -106,6 +156,12 @@ const MODEL_CONTEXT_RULES: readonly ModelContextRule[] = [
|
|
|
106
156
|
mode: "prefix",
|
|
107
157
|
patterns: ["claude-"],
|
|
108
158
|
},
|
|
159
|
+
{
|
|
160
|
+
family: "DeepSeek V4",
|
|
161
|
+
tokens: 1_000_000,
|
|
162
|
+
mode: "prefix",
|
|
163
|
+
patterns: ["deepseek-v4-pro", "deepseek-v4-flash"],
|
|
164
|
+
},
|
|
109
165
|
{
|
|
110
166
|
family: "DeepSeek",
|
|
111
167
|
tokens: 64_000,
|
|
@@ -113,10 +169,16 @@ const MODEL_CONTEXT_RULES: readonly ModelContextRule[] = [
|
|
|
113
169
|
patterns: ["deepseek-"],
|
|
114
170
|
},
|
|
115
171
|
{
|
|
116
|
-
family: "MiniMax",
|
|
172
|
+
family: "MiniMax M3",
|
|
117
173
|
tokens: 1_000_000,
|
|
118
174
|
mode: "includes",
|
|
119
|
-
patterns: ["minimax"],
|
|
175
|
+
patterns: ["minimax-m3"],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
family: "MiniMax M2",
|
|
179
|
+
tokens: 204_800,
|
|
180
|
+
mode: "includes",
|
|
181
|
+
patterns: ["minimax-m2.7", "minimax-m2.5", "minimax-m2.1"],
|
|
120
182
|
},
|
|
121
183
|
];
|
|
122
184
|
|
|
@@ -166,7 +228,11 @@ function matchesRule(model: string, rule: ModelContextRule): boolean {
|
|
|
166
228
|
}
|
|
167
229
|
}
|
|
168
230
|
|
|
169
|
-
export function resolveContextWindow(
|
|
231
|
+
export function resolveContextWindow(
|
|
232
|
+
model: string | null,
|
|
233
|
+
parsed: unknown,
|
|
234
|
+
providers: readonly ProviderConfig[] = [],
|
|
235
|
+
): ContextWindow {
|
|
170
236
|
const explicitWindow = readPositiveIntegerField(parsed, EXPLICIT_CONTEXT_WINDOW_FIELDS);
|
|
171
237
|
if (explicitWindow !== null) {
|
|
172
238
|
return {
|
|
@@ -176,6 +242,15 @@ export function resolveContextWindow(model: string | null, parsed: unknown): Con
|
|
|
176
242
|
};
|
|
177
243
|
}
|
|
178
244
|
|
|
245
|
+
const providerWindow = resolveProviderContextWindow(model, providers);
|
|
246
|
+
if (providerWindow !== null) {
|
|
247
|
+
return {
|
|
248
|
+
tokens: providerWindow.tokens,
|
|
249
|
+
label: `${providerWindow.providerName} config`,
|
|
250
|
+
source: "provider",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
179
254
|
if (model === null) {
|
|
180
255
|
return {
|
|
181
256
|
tokens: null,
|
|
@@ -184,12 +259,12 @@ export function resolveContextWindow(model: string | null, parsed: unknown): Con
|
|
|
184
259
|
};
|
|
185
260
|
}
|
|
186
261
|
|
|
187
|
-
const normalized = model.trim().toLowerCase();
|
|
262
|
+
const normalized = model.trim().toLowerCase().replace(/\s+/g, "-");
|
|
188
263
|
for (const rule of MODEL_CONTEXT_RULES) {
|
|
189
264
|
if (matchesRule(normalized, rule)) {
|
|
190
265
|
return {
|
|
191
266
|
tokens: rule.tokens,
|
|
192
|
-
label: rule.family
|
|
267
|
+
label: `${rule.family} local rule`,
|
|
193
268
|
source: "model-rule",
|
|
194
269
|
};
|
|
195
270
|
}
|
|
@@ -241,51 +316,230 @@ function normalizeDuplicateText(text: string): string {
|
|
|
241
316
|
return text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
242
317
|
}
|
|
243
318
|
|
|
244
|
-
function
|
|
319
|
+
function duplicatePreview(text: string): string {
|
|
320
|
+
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
321
|
+
if (singleLine.length <= DUPLICATE_PREVIEW_CHARS) return singleLine;
|
|
322
|
+
return `${singleLine.slice(0, DUPLICATE_PREVIEW_CHARS - 3)}...`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function duplicateSegmentRef(candidate: DuplicateCandidate): DuplicateContextSegment {
|
|
326
|
+
return {
|
|
327
|
+
role: candidate.role,
|
|
328
|
+
label: candidate.label,
|
|
329
|
+
path: candidate.path,
|
|
330
|
+
tokens: candidate.tokens,
|
|
331
|
+
characters: candidate.characters,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function duplicateCandidateFromSegment(segment: AnatomySegment): DuplicateCandidate {
|
|
336
|
+
return {
|
|
337
|
+
role: segment.role,
|
|
338
|
+
label: segment.label,
|
|
339
|
+
path: segment.path,
|
|
340
|
+
tokens: segment.size,
|
|
341
|
+
characters: segment.characters,
|
|
342
|
+
text: segment.text,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function jsonLikeText(text: string): boolean {
|
|
347
|
+
const trimmed = text.trim();
|
|
348
|
+
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function parseJsonLikeText(text: string): unknown | null {
|
|
352
|
+
if (!jsonLikeText(text)) return null;
|
|
353
|
+
try {
|
|
354
|
+
const parsed: unknown = JSON.parse(text);
|
|
355
|
+
return parsed;
|
|
356
|
+
} catch {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function normalizePathFieldName(field: string): string {
|
|
362
|
+
return field.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function lastNamedPathField(path: readonly string[]): string | null {
|
|
366
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
367
|
+
const field = path[index] ?? "";
|
|
368
|
+
if (field.startsWith("[") && field.endsWith("]")) continue;
|
|
369
|
+
return field;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function nestedDuplicateFieldPath(path: readonly string[]): boolean {
|
|
375
|
+
const field = lastNamedPathField(path);
|
|
376
|
+
if (field === null) return false;
|
|
377
|
+
return DUPLICATE_NESTED_FIELD_NAMES.has(normalizePathFieldName(field));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function formatNestedDuplicatePath(path: readonly string[]): string {
|
|
381
|
+
return path
|
|
382
|
+
.map((part) => (part.startsWith("[") ? part : `.${part}`))
|
|
383
|
+
.join("")
|
|
384
|
+
.replace(/^\./, "");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function duplicateCandidateFromNestedText(
|
|
388
|
+
segment: AnatomySegment,
|
|
389
|
+
text: string,
|
|
390
|
+
path: readonly string[],
|
|
391
|
+
): DuplicateCandidate | null {
|
|
392
|
+
const characters = countCharacters(text);
|
|
393
|
+
if (characters < DUPLICATE_MIN_CHARS) return null;
|
|
394
|
+
const tokens = estimateTokens(text);
|
|
395
|
+
if (tokens < DUPLICATE_MIN_TOKENS) return null;
|
|
396
|
+
return {
|
|
397
|
+
role: segment.role,
|
|
398
|
+
label: `${segment.label} ${formatNestedDuplicatePath(path)}`,
|
|
399
|
+
path: segment.path,
|
|
400
|
+
tokens,
|
|
401
|
+
characters,
|
|
402
|
+
text,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function collectNestedDuplicateCandidates({
|
|
407
|
+
value,
|
|
408
|
+
segment,
|
|
409
|
+
path,
|
|
410
|
+
depth,
|
|
411
|
+
candidates,
|
|
412
|
+
}: {
|
|
413
|
+
value: unknown;
|
|
414
|
+
segment: AnatomySegment;
|
|
415
|
+
path: readonly string[];
|
|
416
|
+
depth: number;
|
|
417
|
+
candidates: DuplicateCandidate[];
|
|
418
|
+
}): void {
|
|
419
|
+
if (candidates.length >= DUPLICATE_NESTED_MAX_CANDIDATES_PER_SEGMENT) return;
|
|
420
|
+
if (depth > DUPLICATE_NESTED_MAX_DEPTH) return;
|
|
421
|
+
|
|
422
|
+
if (typeof value === "string") {
|
|
423
|
+
if (!nestedDuplicateFieldPath(path)) return;
|
|
424
|
+
const candidate = duplicateCandidateFromNestedText(segment, value, path);
|
|
425
|
+
if (candidate !== null) candidates.push(candidate);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (Array.isArray(value)) {
|
|
430
|
+
value.forEach((item, index) =>
|
|
431
|
+
collectNestedDuplicateCandidates({
|
|
432
|
+
value: item,
|
|
433
|
+
segment,
|
|
434
|
+
path: [...path, `[${String(index)}]`],
|
|
435
|
+
depth: depth + 1,
|
|
436
|
+
candidates,
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!isPlainRecord(value)) return;
|
|
443
|
+
Object.entries(value).forEach(([key, child]) =>
|
|
444
|
+
collectNestedDuplicateCandidates({
|
|
445
|
+
value: child,
|
|
446
|
+
segment,
|
|
447
|
+
path: [...path, key],
|
|
448
|
+
depth: depth + 1,
|
|
449
|
+
candidates,
|
|
450
|
+
}),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function duplicateCandidatesForSegment(segment: AnatomySegment): DuplicateCandidate[] {
|
|
455
|
+
const segmentCandidate = duplicateCandidateFromSegment(segment);
|
|
456
|
+
const parsed = parseJsonLikeText(segment.text);
|
|
457
|
+
if (parsed === null) return [segmentCandidate];
|
|
458
|
+
|
|
459
|
+
const candidates: DuplicateCandidate[] = [];
|
|
460
|
+
collectNestedDuplicateCandidates({
|
|
461
|
+
value: parsed,
|
|
462
|
+
segment,
|
|
463
|
+
path: [],
|
|
464
|
+
depth: 0,
|
|
465
|
+
candidates,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const normalizedSegment = normalizeDuplicateText(segment.text);
|
|
469
|
+
const seen = new Set([normalizedSegment]);
|
|
470
|
+
const uniqueNested = candidates.filter((candidate) => {
|
|
471
|
+
const normalized = normalizeDuplicateText(candidate.text);
|
|
472
|
+
if (normalized === "") return false;
|
|
473
|
+
if (seen.has(normalized)) return false;
|
|
474
|
+
seen.add(normalized);
|
|
475
|
+
return true;
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return [segmentCandidate, ...uniqueNested];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function duplicateContextGroups(segments: readonly AnatomySegment[]): DuplicateContextGroup[] {
|
|
245
482
|
type DuplicateBucket = {
|
|
246
|
-
count: number;
|
|
247
483
|
tokens: number;
|
|
248
|
-
|
|
249
|
-
label: string;
|
|
484
|
+
candidates: DuplicateCandidate[];
|
|
250
485
|
};
|
|
251
486
|
|
|
252
487
|
const buckets = new Map<string, DuplicateBucket>();
|
|
253
488
|
for (const segment of segments) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
489
|
+
const candidates = duplicateCandidatesForSegment(segment);
|
|
490
|
+
for (const candidate of candidates) {
|
|
491
|
+
if (candidate.tokens < DUPLICATE_MIN_TOKENS) continue;
|
|
492
|
+
if (candidate.characters < DUPLICATE_MIN_CHARS) continue;
|
|
493
|
+
const normalized = normalizeDuplicateText(candidate.text);
|
|
494
|
+
if (normalized === "") continue;
|
|
495
|
+
const existing = buckets.get(normalized);
|
|
496
|
+
if (existing === undefined) {
|
|
497
|
+
buckets.set(normalized, {
|
|
498
|
+
tokens: candidate.tokens,
|
|
499
|
+
candidates: [candidate],
|
|
500
|
+
});
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
260
503
|
buckets.set(normalized, {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
firstTokens: segment.size,
|
|
264
|
-
label: segment.label,
|
|
504
|
+
tokens: existing.tokens + candidate.tokens,
|
|
505
|
+
candidates: [...existing.candidates, candidate],
|
|
265
506
|
});
|
|
266
|
-
continue;
|
|
267
507
|
}
|
|
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
508
|
}
|
|
275
509
|
|
|
276
|
-
|
|
277
|
-
let repeatedSegments = 0;
|
|
278
|
-
let repeatedTokens = 0;
|
|
279
|
-
let largest: DuplicateBucket | null = null;
|
|
510
|
+
const groups: DuplicateContextGroup[] = [];
|
|
280
511
|
for (const bucket of buckets.values()) {
|
|
281
|
-
if (bucket.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
repeatedTokens
|
|
285
|
-
|
|
512
|
+
if (bucket.candidates.length < 2) continue;
|
|
513
|
+
const firstCandidate = bucket.candidates[0] ?? null;
|
|
514
|
+
if (firstCandidate === null) continue;
|
|
515
|
+
const repeatedTokens = bucket.tokens - firstCandidate.tokens;
|
|
516
|
+
groups.push({
|
|
517
|
+
key: `${firstCandidate.path}:${firstCandidate.label}:${String(
|
|
518
|
+
bucket.candidates.length,
|
|
519
|
+
)}:${String(bucket.tokens)}`,
|
|
520
|
+
count: bucket.candidates.length,
|
|
521
|
+
totalTokens: bucket.tokens,
|
|
522
|
+
repeatedTokens,
|
|
523
|
+
preview: duplicatePreview(firstCandidate.text),
|
|
524
|
+
firstLabel: firstCandidate.label,
|
|
525
|
+
segments: bucket.candidates.map(duplicateSegmentRef),
|
|
526
|
+
});
|
|
286
527
|
}
|
|
287
528
|
|
|
288
|
-
|
|
529
|
+
return groups.sort(
|
|
530
|
+
(left, right) =>
|
|
531
|
+
right.repeatedTokens - left.repeatedTokens || right.totalTokens - left.totalTokens,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function duplicateDiagnostic(
|
|
536
|
+
duplicateGroups: readonly DuplicateContextGroup[],
|
|
537
|
+
): ContextDiagnostic | null {
|
|
538
|
+
if (duplicateGroups.length === 0) return null;
|
|
539
|
+
|
|
540
|
+
const repeatedGroups = duplicateGroups.length;
|
|
541
|
+
const repeatedSegments = duplicateGroups.reduce((sum, group) => sum + group.count, 0);
|
|
542
|
+
const repeatedTokens = duplicateGroups.reduce((sum, group) => sum + group.repeatedTokens, 0);
|
|
289
543
|
|
|
290
544
|
return {
|
|
291
545
|
kind: "duplicate",
|
|
@@ -357,17 +611,19 @@ export function analyzeContextIntelligence({
|
|
|
357
611
|
inputTokens,
|
|
358
612
|
parsed,
|
|
359
613
|
model,
|
|
614
|
+
providers = [],
|
|
360
615
|
}: {
|
|
361
616
|
segments: readonly AnatomySegment[];
|
|
362
617
|
inputTokens: number | null;
|
|
363
618
|
parsed: unknown;
|
|
364
619
|
model: string | null;
|
|
620
|
+
providers?: readonly ProviderConfig[];
|
|
365
621
|
}): ContextIntelligence {
|
|
366
622
|
const totalEstimatedTokens = segments.reduce((sum, segment) => sum + segment.size, 0);
|
|
367
623
|
const requestModel = model ?? readRequestModel(parsed);
|
|
368
624
|
const estimatedInputTokens = inputTokens ?? totalEstimatedTokens;
|
|
369
625
|
const outputReserveTokens = readPositiveIntegerField(parsed, OUTPUT_RESERVE_FIELDS);
|
|
370
|
-
const contextWindow = resolveContextWindow(requestModel, parsed);
|
|
626
|
+
const contextWindow = resolveContextWindow(requestModel, parsed, providers);
|
|
371
627
|
const reservedTokens = outputReserveTokens ?? 0;
|
|
372
628
|
const windowUsedTokens =
|
|
373
629
|
contextWindow.tokens === null ? null : estimatedInputTokens + reservedTokens;
|
|
@@ -384,7 +640,8 @@ export function analyzeContextIntelligence({
|
|
|
384
640
|
const roleUsages = buildRoleUsages(segments, totalEstimatedTokens);
|
|
385
641
|
const largestRole = roleUsages[0] ?? null;
|
|
386
642
|
const messageSegmentCount = segments.filter((segment) => segment.role !== "tools").length;
|
|
387
|
-
const
|
|
643
|
+
const duplicateGroups = duplicateContextGroups(segments);
|
|
644
|
+
const duplicate = duplicateDiagnostic(duplicateGroups);
|
|
388
645
|
const diagnostics = diagnosticsForRolePressure(
|
|
389
646
|
roleUsages,
|
|
390
647
|
totalEstimatedTokens,
|
|
@@ -405,6 +662,7 @@ export function analyzeContextIntelligence({
|
|
|
405
662
|
roleUsages,
|
|
406
663
|
largestRole,
|
|
407
664
|
diagnostics,
|
|
665
|
+
duplicateGroups,
|
|
408
666
|
};
|
|
409
667
|
}
|
|
410
668
|
|
|
@@ -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>;
|