@tonyclaw/agent-inspector 2.0.10 → 2.0.12
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-BWMipEjS.js → CompareDrawer-ClM_uVRy.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CTNNzXSa.js +114 -0
- package/.output/public/assets/{ReplayDialog-DaSzZsvM.js → ReplayDialog-7rp7N_KJ.js} +1 -1
- package/.output/public/assets/RequestAnatomy-sL2FFo_N.js +1 -0
- package/.output/public/assets/ResponseView-Cjlf3b6-.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-B4atnL99.js → StreamingChunkSequence-Ctx3sT3r.js} +1 -1
- package/.output/public/assets/_sessionId-DEWzDQ8-.js +1 -0
- package/.output/public/assets/index-DGIIhZTn.js +1 -0
- package/.output/public/assets/index-D_nZj9Vt.css +1 -0
- package/.output/public/assets/{main-DE6Y4-wV.js → main-FvIQopAy.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +51 -39
- package/.output/server/{_sessionId-61HO9rUI.mjs → _sessionId-BDSgtrsI.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-CSPqneYm.mjs → CompareDrawer-Cqvv0I3x.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-DrSMa5O7.mjs → ProxyViewerContainer-C0YEoG8N.mjs} +1611 -183
- package/.output/server/_ssr/{ReplayDialog-Dn3naENv.mjs → ReplayDialog-CrsynUKI.mjs} +4 -4
- package/.output/server/_ssr/RequestAnatomy-CR_JUkcL.mjs +688 -0
- package/.output/server/_ssr/{ResponseView-CwXAWOoE.mjs → ResponseView-Dba2Et69.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-DRSR2FfN.mjs → StreamingChunkSequence-BIMHLstI.mjs} +2 -2
- package/.output/server/_ssr/{index-D4zn5CAg.mjs → index-DveIUTm5.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-CSRaa_tu.mjs → router-D0yXVG0Q.mjs} +85 -22
- package/.output/server/{_tanstack-start-manifest_v-CfvVUYYj.mjs → _tanstack-start-manifest_v-pe0xJFuQ.mjs} +1 -1
- package/.output/server/index.mjs +70 -70
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +453 -52
- package/src/components/providers/ProviderCard.tsx +45 -2
- package/src/components/providers/ProviderForm.tsx +245 -80
- package/src/components/providers/ProvidersPanel.tsx +7 -1
- package/src/components/proxy-viewer/ConversationGroup.tsx +18 -1
- package/src/components/proxy-viewer/LogEntry.tsx +25 -0
- package/src/components/proxy-viewer/TurnGroup.tsx +15 -0
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +144 -80
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +70 -5
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +196 -0
- package/src/components/proxy-viewer/logFocus.ts +33 -0
- package/src/lib/providerModelMetadata.ts +64 -12
- package/src/lib/utils.ts +7 -0
- package/src/proxy/providers.ts +25 -0
- package/src/routes/api/providers.ts +3 -0
- package/.output/public/assets/ProxyViewerContainer-DIAgurux.js +0 -114
- package/.output/public/assets/RequestAnatomy-BfQmPHCd.js +0 -1
- package/.output/public/assets/ResponseView-CblEWDMx.js +0 -1
- package/.output/public/assets/_sessionId-DUzYnZXV.js +0 -1
- package/.output/public/assets/index-CxhRsekH.css +0 -1
- package/.output/public/assets/index-jagRsQaV.js +0 -1
- package/.output/server/_ssr/RequestAnatomy-D1X3f3AX.mjs +0 -1322
|
@@ -2,7 +2,7 @@ import { type JSX, useState, useEffect, useRef } from "react";
|
|
|
2
2
|
import { Button } from "../ui/button";
|
|
3
3
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
4
4
|
import { Eye, EyeOff, Copy, Check, ChevronDown } from "lucide-react";
|
|
5
|
-
import type { ProviderConfig } from "../../
|
|
5
|
+
import type { ProviderConfig, ProviderModelMetadata } from "../../lib/providerContract";
|
|
6
6
|
import { maskApiKey } from "../../lib/mask";
|
|
7
7
|
|
|
8
8
|
// Known provider presets - maps provider name keywords to their API URLs
|
|
@@ -38,7 +38,7 @@ const MINIMAX_MODELS = [
|
|
|
38
38
|
];
|
|
39
39
|
|
|
40
40
|
// Alibaba model options
|
|
41
|
-
const ALIBABA_MODELS = ["glm-5", "glm-5.1", "qwen3.6-plus", "qwen3.7-max"];
|
|
41
|
+
const ALIBABA_MODELS = ["glm-5", "glm-5.1", "glm-5.2", "qwen3.6-plus", "qwen3.7-max"];
|
|
42
42
|
|
|
43
43
|
type ProviderFormProps = {
|
|
44
44
|
provider?: ProviderConfig;
|
|
@@ -50,11 +50,58 @@ type ProviderFormProps = {
|
|
|
50
50
|
openaiBaseUrl?: string;
|
|
51
51
|
apiDocsUrl?: string;
|
|
52
52
|
modelMetadataUrl?: string;
|
|
53
|
+
modelMetadata?: ProviderModelMetadata[];
|
|
53
54
|
source?: "company" | "personal";
|
|
54
55
|
}) => void;
|
|
55
56
|
onCancel: () => void;
|
|
56
57
|
};
|
|
57
58
|
|
|
59
|
+
type ModelMetadataDraft = {
|
|
60
|
+
contextWindow: string;
|
|
61
|
+
outputLimit: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function emptyMetadataDraft(): ModelMetadataDraft {
|
|
65
|
+
return { contextWindow: "", outputLimit: "" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeModelName(value: string): string {
|
|
69
|
+
return value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findModelMetadata(
|
|
73
|
+
provider: ProviderConfig | undefined,
|
|
74
|
+
model: string,
|
|
75
|
+
): ProviderModelMetadata | null {
|
|
76
|
+
const normalized = normalizeModelName(model);
|
|
77
|
+
return (
|
|
78
|
+
provider?.modelMetadata?.find(
|
|
79
|
+
(metadata) => normalizeModelName(metadata.model) === normalized,
|
|
80
|
+
) ?? null
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createMetadataDrafts(
|
|
85
|
+
provider: ProviderConfig | undefined,
|
|
86
|
+
models: readonly string[],
|
|
87
|
+
): ModelMetadataDraft[] {
|
|
88
|
+
return models.map((model) => {
|
|
89
|
+
const metadata = findModelMetadata(provider, model);
|
|
90
|
+
return {
|
|
91
|
+
contextWindow: metadata?.contextWindow?.toString() ?? "",
|
|
92
|
+
outputLimit: metadata?.outputLimit?.toString() ?? "",
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readPositiveIntegerInput(value: string): number | null {
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
if (trimmed === "") return null;
|
|
100
|
+
const parsed = Number(trimmed);
|
|
101
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return null;
|
|
102
|
+
return parsed;
|
|
103
|
+
}
|
|
104
|
+
|
|
58
105
|
export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps): JSX.Element {
|
|
59
106
|
const [name, setName] = useState(provider?.name ?? "");
|
|
60
107
|
const [apiKey, setApiKey] = useState(provider?.apiKey ?? "");
|
|
@@ -64,6 +111,12 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
64
111
|
const [models, setModels] = useState<string[]>(
|
|
65
112
|
initialModels !== undefined && initialModels.length > 0 ? initialModels : [""],
|
|
66
113
|
);
|
|
114
|
+
const [modelMetadataDrafts, setModelMetadataDrafts] = useState<ModelMetadataDraft[]>(() =>
|
|
115
|
+
createMetadataDrafts(
|
|
116
|
+
provider,
|
|
117
|
+
initialModels !== undefined && initialModels.length > 0 ? initialModels : [""],
|
|
118
|
+
),
|
|
119
|
+
);
|
|
67
120
|
const [activeTab, setActiveTab] = useState<"anthropic" | "openai">("anthropic");
|
|
68
121
|
const [anthropicBaseUrl, setAnthropicBaseUrl] = useState(provider?.anthropicBaseUrl ?? "");
|
|
69
122
|
const [openaiBaseUrl, setOpenaiBaseUrl] = useState(provider?.openaiBaseUrl ?? "");
|
|
@@ -111,7 +164,9 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
111
164
|
if (provider) {
|
|
112
165
|
setName(provider.name);
|
|
113
166
|
setApiKey(provider.apiKey);
|
|
114
|
-
|
|
167
|
+
const nextModels = (provider.models?.length ?? 0) > 0 ? provider.models : [""];
|
|
168
|
+
setModels(nextModels);
|
|
169
|
+
setModelMetadataDrafts(createMetadataDrafts(provider, nextModels));
|
|
115
170
|
setAnthropicBaseUrl(provider.anthropicBaseUrl ?? "");
|
|
116
171
|
setOpenaiBaseUrl(provider.openaiBaseUrl ?? "");
|
|
117
172
|
setApiDocsUrl(provider.apiDocsUrl ?? "");
|
|
@@ -168,6 +223,21 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
168
223
|
if (modelMetadataUrl.trim() && !isValidUrl(modelMetadataUrl.trim())) {
|
|
169
224
|
newErrors.modelMetadataUrl = "Invalid URL format";
|
|
170
225
|
}
|
|
226
|
+
modelMetadataDrafts.forEach((draft, index) => {
|
|
227
|
+
const contextWindow = readPositiveIntegerInput(draft.contextWindow);
|
|
228
|
+
const outputLimit = readPositiveIntegerInput(draft.outputLimit);
|
|
229
|
+
if (draft.contextWindow.trim() !== "" && contextWindow === null) {
|
|
230
|
+
newErrors[`modelMetadata.${index}.contextWindow`] =
|
|
231
|
+
"Context window must be a positive integer";
|
|
232
|
+
}
|
|
233
|
+
if (draft.outputLimit.trim() !== "" && outputLimit === null) {
|
|
234
|
+
newErrors[`modelMetadata.${index}.outputLimit`] = "Output limit must be a positive integer";
|
|
235
|
+
}
|
|
236
|
+
if (contextWindow !== null && outputLimit !== null && outputLimit > contextWindow) {
|
|
237
|
+
newErrors[`modelMetadata.${index}.outputLimit`] =
|
|
238
|
+
"Output limit should not exceed context window";
|
|
239
|
+
}
|
|
240
|
+
});
|
|
171
241
|
if (!anthropicBaseUrl.trim() && !openaiBaseUrl.trim()) {
|
|
172
242
|
newErrors.format = "At least one format URL (Anthropic or OpenAI) is required";
|
|
173
243
|
}
|
|
@@ -184,6 +254,68 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
184
254
|
}
|
|
185
255
|
}
|
|
186
256
|
|
|
257
|
+
function updateModel(index: number, value: string): void {
|
|
258
|
+
setModels((prev) => {
|
|
259
|
+
const next = [...prev];
|
|
260
|
+
next[index] = value;
|
|
261
|
+
return next;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function updateMetadataDraft(index: number, patch: Partial<ModelMetadataDraft>): void {
|
|
266
|
+
setModelMetadataDrafts((prev) => {
|
|
267
|
+
const next = [...prev];
|
|
268
|
+
next[index] = { ...(next[index] ?? emptyMetadataDraft()), ...patch };
|
|
269
|
+
return next;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function removeModel(index: number): void {
|
|
274
|
+
setModels((prev) => prev.filter((_, idx) => idx !== index));
|
|
275
|
+
setModelMetadataDrafts((prev) => prev.filter((_, idx) => idx !== index));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function addModel(): void {
|
|
279
|
+
setModels((prev) => [...prev, ""]);
|
|
280
|
+
setModelMetadataDrafts((prev) => [...prev, emptyMetadataDraft()]);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildModelMetadata(): ProviderModelMetadata[] | undefined {
|
|
284
|
+
const now = new Date().toISOString();
|
|
285
|
+
const metadata: ProviderModelMetadata[] = [];
|
|
286
|
+
|
|
287
|
+
models.forEach((model, index) => {
|
|
288
|
+
const trimmedModel = model.trim();
|
|
289
|
+
if (trimmedModel === "") return;
|
|
290
|
+
const draft = modelMetadataDrafts[index] ?? emptyMetadataDraft();
|
|
291
|
+
const contextWindow = readPositiveIntegerInput(draft.contextWindow);
|
|
292
|
+
const outputLimit = readPositiveIntegerInput(draft.outputLimit);
|
|
293
|
+
if (contextWindow === null && outputLimit === null) return;
|
|
294
|
+
|
|
295
|
+
const existing = findModelMetadata(provider, trimmedModel);
|
|
296
|
+
if (
|
|
297
|
+
existing !== null &&
|
|
298
|
+
existing.contextWindow === (contextWindow ?? undefined) &&
|
|
299
|
+
existing.outputLimit === (outputLimit ?? undefined)
|
|
300
|
+
) {
|
|
301
|
+
metadata.push({ ...existing, model: trimmedModel });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const entry: ProviderModelMetadata = {
|
|
306
|
+
model: trimmedModel,
|
|
307
|
+
source: "manual",
|
|
308
|
+
updatedAt: now,
|
|
309
|
+
};
|
|
310
|
+
if (contextWindow !== null) entry.contextWindow = contextWindow;
|
|
311
|
+
if (outputLimit !== null) entry.outputLimit = outputLimit;
|
|
312
|
+
metadata.push(entry);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (metadata.length > 0) return metadata;
|
|
316
|
+
return provider?.modelMetadata !== undefined ? [] : undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
187
319
|
function handleSubmit(e: React.FormEvent): void {
|
|
188
320
|
e.preventDefault();
|
|
189
321
|
if (!validate()) return;
|
|
@@ -197,6 +329,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
197
329
|
openaiBaseUrl: openaiBaseUrl.trim() || undefined,
|
|
198
330
|
apiDocsUrl: apiDocsUrl.trim() || undefined,
|
|
199
331
|
modelMetadataUrl: modelMetadataUrl.trim() || undefined,
|
|
332
|
+
modelMetadata: buildModelMetadata(),
|
|
200
333
|
source,
|
|
201
334
|
});
|
|
202
335
|
} finally {
|
|
@@ -320,91 +453,123 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
320
453
|
<label className="text-sm font-medium">
|
|
321
454
|
Models <span className="text-destructive">*</span>
|
|
322
455
|
</label>
|
|
323
|
-
{models.map((m, i) =>
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
456
|
+
{models.map((m, i) => {
|
|
457
|
+
const draft = modelMetadataDrafts[i] ?? emptyMetadataDraft();
|
|
458
|
+
const contextWindowError = errors[`modelMetadata.${i}.contextWindow`];
|
|
459
|
+
const outputLimitError = errors[`modelMetadata.${i}.outputLimit`];
|
|
460
|
+
return (
|
|
461
|
+
<div
|
|
462
|
+
key={i}
|
|
463
|
+
ref={(el) => {
|
|
464
|
+
modelRowRefs.current[i] = el;
|
|
465
|
+
}}
|
|
466
|
+
className="space-y-2 rounded-md border border-border bg-muted/10 p-2"
|
|
467
|
+
>
|
|
468
|
+
<div className="flex items-center gap-2">
|
|
469
|
+
<div className="relative flex-1">
|
|
470
|
+
<input
|
|
471
|
+
type="text"
|
|
472
|
+
value={m}
|
|
473
|
+
onChange={(e) => updateModel(i, e.target.value)}
|
|
474
|
+
placeholder={
|
|
475
|
+
isMiniMax || isAlibaba ? "Type or select a model..." : "Model name"
|
|
476
|
+
}
|
|
477
|
+
className="w-full rounded-md border border-input bg-background px-4 py-3 pr-8 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
|
|
478
|
+
/>
|
|
479
|
+
{(isMiniMax || isAlibaba) && (
|
|
480
|
+
<>
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
onClick={() => setOpenModelDropdown(openModelDropdown === i ? null : i)}
|
|
484
|
+
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors p-1 z-10"
|
|
485
|
+
aria-label="Show model suggestions"
|
|
486
|
+
>
|
|
487
|
+
<ChevronDown className="size-4" />
|
|
488
|
+
</button>
|
|
489
|
+
{openModelDropdown === i && (
|
|
490
|
+
<div className="absolute left-0 right-0 top-full mt-1 z-50 bg-popover border border-border rounded-md shadow-md max-h-48 overflow-y-auto">
|
|
491
|
+
{(isMiniMax ? MINIMAX_MODELS : ALIBABA_MODELS).map((opt) => (
|
|
492
|
+
<button
|
|
493
|
+
key={opt}
|
|
494
|
+
type="button"
|
|
495
|
+
onClick={() => {
|
|
496
|
+
updateModel(i, opt);
|
|
497
|
+
setOpenModelDropdown(null);
|
|
498
|
+
}}
|
|
499
|
+
className="w-full text-left px-3 py-2 text-sm hover:bg-muted transition-colors"
|
|
500
|
+
>
|
|
501
|
+
{opt}
|
|
502
|
+
</button>
|
|
503
|
+
))}
|
|
504
|
+
</div>
|
|
505
|
+
)}
|
|
506
|
+
</>
|
|
507
|
+
)}
|
|
508
|
+
</div>
|
|
509
|
+
{models.length > 1 && (
|
|
347
510
|
<button
|
|
348
511
|
type="button"
|
|
349
|
-
onClick={() =>
|
|
350
|
-
className="
|
|
351
|
-
aria-label="
|
|
512
|
+
onClick={() => removeModel(i)}
|
|
513
|
+
className="text-muted-foreground hover:text-destructive transition-colors p-1 shrink-0"
|
|
514
|
+
aria-label="Remove model"
|
|
352
515
|
>
|
|
353
|
-
<
|
|
516
|
+
<svg
|
|
517
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
518
|
+
width="16"
|
|
519
|
+
height="16"
|
|
520
|
+
viewBox="0 0 24 24"
|
|
521
|
+
fill="none"
|
|
522
|
+
stroke="currentColor"
|
|
523
|
+
strokeWidth="2"
|
|
524
|
+
strokeLinecap="round"
|
|
525
|
+
strokeLinejoin="round"
|
|
526
|
+
>
|
|
527
|
+
<path d="M3 6h18" />
|
|
528
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
529
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
530
|
+
</svg>
|
|
354
531
|
</button>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
532
|
+
)}
|
|
533
|
+
</div>
|
|
534
|
+
|
|
535
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
536
|
+
<label className="space-y-1 text-xs text-muted-foreground">
|
|
537
|
+
<span>Context Window</span>
|
|
538
|
+
<input
|
|
539
|
+
type="number"
|
|
540
|
+
min={1}
|
|
541
|
+
step={1000}
|
|
542
|
+
value={draft.contextWindow}
|
|
543
|
+
onChange={(e) => updateMetadataDraft(i, { contextWindow: e.target.value })}
|
|
544
|
+
placeholder="e.g. 1048576"
|
|
545
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-xs text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px]"
|
|
546
|
+
/>
|
|
547
|
+
{contextWindowError !== undefined && (
|
|
548
|
+
<span className="block text-destructive">{contextWindowError}</span>
|
|
549
|
+
)}
|
|
550
|
+
</label>
|
|
551
|
+
<label className="space-y-1 text-xs text-muted-foreground">
|
|
552
|
+
<span>Output Limit</span>
|
|
553
|
+
<input
|
|
554
|
+
type="number"
|
|
555
|
+
min={1}
|
|
556
|
+
step={1000}
|
|
557
|
+
value={draft.outputLimit}
|
|
558
|
+
onChange={(e) => updateMetadataDraft(i, { outputLimit: e.target.value })}
|
|
559
|
+
placeholder="e.g. 8192"
|
|
560
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-xs text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px]"
|
|
561
|
+
/>
|
|
562
|
+
{outputLimitError !== undefined && (
|
|
563
|
+
<span className="block text-destructive">{outputLimitError}</span>
|
|
375
564
|
)}
|
|
376
|
-
|
|
377
|
-
|
|
565
|
+
</label>
|
|
566
|
+
</div>
|
|
378
567
|
</div>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
type="button"
|
|
382
|
-
onClick={() => setModels((prev) => prev.filter((_, idx) => idx !== i))}
|
|
383
|
-
className="text-muted-foreground hover:text-destructive transition-colors p-1 shrink-0"
|
|
384
|
-
aria-label="Remove model"
|
|
385
|
-
>
|
|
386
|
-
<svg
|
|
387
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
388
|
-
width="16"
|
|
389
|
-
height="16"
|
|
390
|
-
viewBox="0 0 24 24"
|
|
391
|
-
fill="none"
|
|
392
|
-
stroke="currentColor"
|
|
393
|
-
strokeWidth="2"
|
|
394
|
-
strokeLinecap="round"
|
|
395
|
-
strokeLinejoin="round"
|
|
396
|
-
>
|
|
397
|
-
<path d="M3 6h18" />
|
|
398
|
-
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
399
|
-
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
400
|
-
</svg>
|
|
401
|
-
</button>
|
|
402
|
-
)}
|
|
403
|
-
</div>
|
|
404
|
-
))}
|
|
568
|
+
);
|
|
569
|
+
})}
|
|
405
570
|
<button
|
|
406
571
|
type="button"
|
|
407
|
-
onClick={
|
|
572
|
+
onClick={addModel}
|
|
408
573
|
className="text-xs text-primary hover:underline flex items-center gap-1"
|
|
409
574
|
>
|
|
410
575
|
+ Add Model
|
|
@@ -8,7 +8,11 @@ import { ConfirmDialog } from "../ui/confirm-dialog";
|
|
|
8
8
|
import { ProviderCard } from "./ProviderCard";
|
|
9
9
|
import { ProviderForm } from "./ProviderForm";
|
|
10
10
|
import { parseJsonResponse, readApiError } from "../../lib/apiClient";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ProviderConfigSchema,
|
|
13
|
+
type ProviderConfig,
|
|
14
|
+
type ProviderModelMetadata,
|
|
15
|
+
} from "../../lib/providerContract";
|
|
12
16
|
import {
|
|
13
17
|
createFailedProviderTestResults,
|
|
14
18
|
createPendingProviderTestResults,
|
|
@@ -48,6 +52,7 @@ type ProviderFormData = {
|
|
|
48
52
|
openaiBaseUrl?: string;
|
|
49
53
|
apiDocsUrl?: string;
|
|
50
54
|
modelMetadataUrl?: string;
|
|
55
|
+
modelMetadata?: ProviderModelMetadata[];
|
|
51
56
|
source?: "company" | "personal";
|
|
52
57
|
};
|
|
53
58
|
|
|
@@ -60,6 +65,7 @@ function createProviderPayload(data: ProviderFormData) {
|
|
|
60
65
|
openaiBaseUrl: (data.openaiBaseUrl?.length ?? 0) > 0 ? data.openaiBaseUrl : undefined,
|
|
61
66
|
apiDocsUrl: (data.apiDocsUrl?.length ?? 0) > 0 ? data.apiDocsUrl : undefined,
|
|
62
67
|
modelMetadataUrl: (data.modelMetadataUrl?.length ?? 0) > 0 ? data.modelMetadataUrl : undefined,
|
|
68
|
+
modelMetadata: data.modelMetadata,
|
|
63
69
|
source: data.source,
|
|
64
70
|
};
|
|
65
71
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, memo, useMemo } from "react";
|
|
1
|
+
import { useState, memo, useEffect, useMemo } from "react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
4
4
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { TurnGroup } from "./TurnGroup";
|
|
12
12
|
import { AgentTraceSummary } from "./AgentTraceSummary";
|
|
13
13
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
14
|
+
import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
|
|
14
15
|
import { buildTurnGroups, shouldRenderConversationContent } from "./viewerState";
|
|
15
16
|
|
|
16
17
|
export type ConversationGroupProps = {
|
|
@@ -74,6 +75,22 @@ export const ConversationGroup = memo(function ({
|
|
|
74
75
|
|
|
75
76
|
// Pre-compute stop reasons for each log — used by turnIndices
|
|
76
77
|
const turnGroups = useMemo(() => buildTurnGroups(group.logs), [group.logs]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const handleLogFocusRequest = (event: Event): void => {
|
|
81
|
+
if (standalone) return;
|
|
82
|
+
const request = readLogFocusRequest(event);
|
|
83
|
+
if (request === null) return;
|
|
84
|
+
if (!group.logs.some((log) => log.id === request.logId)) return;
|
|
85
|
+
setExpanded(true);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
window.addEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
89
|
+
return () => {
|
|
90
|
+
window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
91
|
+
};
|
|
92
|
+
}, [group.logs, standalone]);
|
|
93
|
+
|
|
77
94
|
return (
|
|
78
95
|
<div className="mb-2">
|
|
79
96
|
{!standalone && (
|
|
@@ -25,6 +25,7 @@ import { LogEntryHeader } from "./LogEntryHeader";
|
|
|
25
25
|
import { RequestTools } from "./RequestToolsPanel";
|
|
26
26
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
27
27
|
import { getLogFormatAdapter, resolveLogFormat } from "./log-formats";
|
|
28
|
+
import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest, type LogFocusTab } from "./logFocus";
|
|
28
29
|
import {
|
|
29
30
|
shouldShowHeadersDiffButton,
|
|
30
31
|
shouldShowRawRequestTab,
|
|
@@ -40,6 +41,15 @@ function TabFallback(): JSX.Element {
|
|
|
40
41
|
return <div className="h-1" aria-hidden="true" />;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function resolveFocusedTab(tab: LogFocusTab, anatomySegments: AnatomySegment[] | null): string {
|
|
45
|
+
switch (tab) {
|
|
46
|
+
case "anatomy":
|
|
47
|
+
return anatomySegments === null ? "request" : "anatomy";
|
|
48
|
+
case "request":
|
|
49
|
+
return "request";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
export type LogEntryProps = {
|
|
44
54
|
log: CapturedLog;
|
|
45
55
|
viewMode?: "simple" | "full";
|
|
@@ -302,6 +312,21 @@ export const LogEntry = memo(function ({
|
|
|
302
312
|
setActiveTab(anatomySegments !== null ? "anatomy" : "request");
|
|
303
313
|
}, [anatomySegments, expanded]);
|
|
304
314
|
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
const handleLogFocusRequest = (event: Event): void => {
|
|
317
|
+
const request = readLogFocusRequest(event);
|
|
318
|
+
if (request === null) return;
|
|
319
|
+
if (request.logId !== log.id) return;
|
|
320
|
+
setExpanded(true);
|
|
321
|
+
setActiveTab(resolveFocusedTab(request.tab, anatomySegments));
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
window.addEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
325
|
+
return () => {
|
|
326
|
+
window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
327
|
+
};
|
|
328
|
+
}, [anatomySegments, log.id]);
|
|
329
|
+
|
|
305
330
|
return (
|
|
306
331
|
<TooltipProvider>
|
|
307
332
|
<div className="border border-border rounded-lg mb-1 overflow-hidden">
|
|
@@ -9,6 +9,7 @@ import { ProviderLogo, detectProvider, type Provider } from "../providers/Provid
|
|
|
9
9
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
|
|
10
10
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
11
11
|
import { LogEntry } from "./LogEntry";
|
|
12
|
+
import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
|
|
12
13
|
import { ThreadConnector } from "./ThreadConnector";
|
|
13
14
|
import { ToolTraceEvents } from "./ToolTraceEvents";
|
|
14
15
|
import { extractToolTraceEvents, isTurnCollapsible, type TurnEntry } from "./viewerState";
|
|
@@ -60,6 +61,20 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
60
61
|
prevCompleteRef.current = isComplete;
|
|
61
62
|
}, [collapsible, isComplete]);
|
|
62
63
|
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const handleLogFocusRequest = (event: Event): void => {
|
|
66
|
+
const request = readLogFocusRequest(event);
|
|
67
|
+
if (request === null) return;
|
|
68
|
+
if (!entries.some((entry) => entry.log.id === request.logId)) return;
|
|
69
|
+
if (collapsible) setCollapsed(false);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
window.addEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
73
|
+
return () => {
|
|
74
|
+
window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
75
|
+
};
|
|
76
|
+
}, [collapsible, entries]);
|
|
77
|
+
|
|
63
78
|
const toggleCollapse = useCallback(() => {
|
|
64
79
|
if (collapsible) setCollapsed((prev) => !prev);
|
|
65
80
|
}, [collapsible]);
|