@tonyclaw/agent-inspector 2.1.15 → 2.1.17
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/cli.js +57 -13
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-B9sLBHw5.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
- package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-D9I9W9n3.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-DGXK_Rii.js} +1 -1
- package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-df_JzwsS.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-i3DM5IKL.js} +1 -1
- package/.output/public/assets/_sessionId-CTpCHdFh.js +1 -0
- package/.output/public/assets/index-Blqvndcn.js +1 -0
- package/.output/public/assets/index-Cs79WRQj.css +1 -0
- package/.output/public/assets/{index-DjKt8XKe.js → index-Dqc2r1ea.js} +1 -1
- package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-D3QWQlSB.js} +1 -1
- package/.output/public/assets/{main-DpD1N0S8.js → main-ZhxhOCmF.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +255 -201
- package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +2732 -1849
- package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-HQfrxjO1.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-QSlSD11e.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +3 -3
- package/.output/server/_ssr/{index-47XVPghS.mjs → index-D_EzfXaN.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
- package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-__o2wrfO.mjs} +505 -136
- package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/README.md +57 -2
- package/package.json +1 -1
- package/src/cli/startupOutput.ts +16 -0
- package/src/cli.ts +61 -15
- package/src/components/ProxyViewer.tsx +535 -163
- package/src/components/ProxyViewerContainer.tsx +77 -67
- package/src/components/alerts/AlertsDialog.tsx +2 -1
- package/src/components/clients/ClientLogo.tsx +1 -1
- package/src/components/groups/GroupsDialog.tsx +19 -1
- package/src/components/providers/ProviderCard.tsx +133 -32
- package/src/components/providers/ProviderForm.tsx +61 -29
- package/src/components/providers/ProviderLogo.tsx +6 -1
- package/src/components/providers/ProvidersPanel.tsx +7 -7
- package/src/components/providers/SettingsDialog.tsx +13 -6
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
- package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
- package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
- package/src/components/proxy-viewer/ConversationHeader.tsx +153 -120
- package/src/components/proxy-viewer/LogEntry.tsx +130 -43
- package/src/components/proxy-viewer/LogEntryHeader.tsx +357 -354
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
- package/src/components/proxy-viewer/ThreadConnector.tsx +102 -83
- package/src/components/proxy-viewer/TurnGroup.tsx +33 -69
- package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
- package/src/components/proxy-viewer/bodyHydration.ts +56 -0
- package/src/components/proxy-viewer/viewerState.ts +0 -4
- package/src/components/ui/icon-trigger.ts +2 -0
- package/src/components/ui/transient-toast.tsx +104 -0
- package/src/lib/providerTestContract.ts +56 -10
- package/src/lib/providerTestPrompt.ts +23 -1
- package/src/lib/sessionInfoContract.ts +4 -0
- package/src/lib/upstreamUrl.ts +105 -1
- package/src/mcp/server.ts +71 -27
- package/src/mcp/toolHandlers.ts +24 -5
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
- package/src/proxy/sessionInfo.ts +31 -2
- package/src/proxy/sessionSupervisor.ts +20 -1
- package/src/proxy/store.ts +1 -1
- package/src/routes/api/providers.$providerId.test.log.ts +93 -14
- package/styles/globals.css +180 -180
- package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
- package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
- package/.output/public/assets/index-CI1-G8ua.js +0 -1
- package/.output/public/assets/index-DdhFqPsI.css +0 -1
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type SessionMembershipEvidence,
|
|
13
13
|
} from "./ProxyViewer";
|
|
14
14
|
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
15
|
+
import { TransientToast, useTransientToast } from "./ui/transient-toast";
|
|
15
16
|
|
|
16
17
|
type SSEUpdate =
|
|
17
18
|
| {
|
|
@@ -185,7 +186,7 @@ export function ProxyViewerContainer({
|
|
|
185
186
|
const [allLogs, setAllLogs] = useState<CapturedLog[]>([]);
|
|
186
187
|
const [selectedSession, setSelectedSession] = useState(initialSessionId ?? "__all__");
|
|
187
188
|
const [selectedModel, setSelectedModel] = useState("__all__");
|
|
188
|
-
const
|
|
189
|
+
const { notice, showToast, dismissToast } = useTransientToast();
|
|
189
190
|
const [streamInitialized, setStreamInitialized] = useState(initialSessionId === undefined);
|
|
190
191
|
const [logPage, setLogPage] = useState<LogCursorPage | null>(null);
|
|
191
192
|
const [sessionPageLoading, setSessionPageLoading] = useState(initialSessionId !== undefined);
|
|
@@ -202,6 +203,12 @@ export function ProxyViewerContainer({
|
|
|
202
203
|
// Debounce buffer for SSE updates
|
|
203
204
|
const pendingUpdatesRef = useRef<CapturedLog[]>([]);
|
|
204
205
|
const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
206
|
+
const showErrorToast = useCallback(
|
|
207
|
+
(message: string) => {
|
|
208
|
+
showToast({ message, tone: "error" });
|
|
209
|
+
},
|
|
210
|
+
[showToast],
|
|
211
|
+
);
|
|
205
212
|
|
|
206
213
|
// Derived view: the logs the user actually sees. Recomputed only when the
|
|
207
214
|
// underlying set or filter changes — never on every SSE message.
|
|
@@ -269,7 +276,7 @@ export function ProxyViewerContainer({
|
|
|
269
276
|
const parsed: unknown = JSON.parse(rawData);
|
|
270
277
|
const updateResult = SSEUpdateSchema.safeParse(parsed);
|
|
271
278
|
if (!updateResult.success) {
|
|
272
|
-
|
|
279
|
+
showErrorToast("Failed to parse SSE data");
|
|
273
280
|
return;
|
|
274
281
|
}
|
|
275
282
|
const update = updateResult.data;
|
|
@@ -285,17 +292,17 @@ export function ProxyViewerContainer({
|
|
|
285
292
|
logIndexRef.current = buildLogIndex(nextLogs);
|
|
286
293
|
setAllLogs(nextLogs);
|
|
287
294
|
setStreamInitialized(true);
|
|
288
|
-
|
|
295
|
+
dismissToast();
|
|
289
296
|
} else if (update.type === "update") {
|
|
290
297
|
scheduleUpdate(update.log);
|
|
291
298
|
}
|
|
292
299
|
} catch {
|
|
293
|
-
|
|
300
|
+
showErrorToast("Failed to parse SSE data");
|
|
294
301
|
}
|
|
295
302
|
};
|
|
296
303
|
|
|
297
304
|
es.onerror = () => {
|
|
298
|
-
|
|
305
|
+
showErrorToast("SSE connection lost, reconnecting...");
|
|
299
306
|
setStreamInitialized(true);
|
|
300
307
|
es.close();
|
|
301
308
|
if (reconnectTimeoutRef.current !== null) {
|
|
@@ -303,7 +310,7 @@ export function ProxyViewerContainer({
|
|
|
303
310
|
}
|
|
304
311
|
reconnectTimeoutRef.current = setTimeout(connectSSE, 3000);
|
|
305
312
|
};
|
|
306
|
-
}, [initialSessionId, scheduleUpdate]);
|
|
313
|
+
}, [dismissToast, initialSessionId, scheduleUpdate, showErrorToast]);
|
|
307
314
|
|
|
308
315
|
const loadSessionPage = useCallback(
|
|
309
316
|
(request: SessionPageRequest) => {
|
|
@@ -312,7 +319,7 @@ export function ProxyViewerContainer({
|
|
|
312
319
|
const requestId = sessionPageRequestIdRef.current + 1;
|
|
313
320
|
sessionPageRequestIdRef.current = requestId;
|
|
314
321
|
setSessionPageLoading(true);
|
|
315
|
-
|
|
322
|
+
dismissToast();
|
|
316
323
|
|
|
317
324
|
void fetchJson(buildSessionLogsPageUrl(initialSessionId, request), LogCursorPageSchema)
|
|
318
325
|
.then((page) => {
|
|
@@ -323,17 +330,17 @@ export function ProxyViewerContainer({
|
|
|
323
330
|
setLogPage(page);
|
|
324
331
|
setSelectedSession(initialSessionId);
|
|
325
332
|
setSessionPageLoading(false);
|
|
326
|
-
|
|
333
|
+
dismissToast();
|
|
327
334
|
})
|
|
328
335
|
.catch((err: unknown) => {
|
|
329
336
|
if (sessionPageRequestIdRef.current !== requestId) return;
|
|
330
337
|
setAllLogs([]);
|
|
331
338
|
setLogPage(null);
|
|
332
339
|
setSessionPageLoading(false);
|
|
333
|
-
|
|
340
|
+
showErrorToast(err instanceof Error ? err.message : "Failed to load session logs");
|
|
334
341
|
});
|
|
335
342
|
},
|
|
336
|
-
[initialSessionId],
|
|
343
|
+
[dismissToast, initialSessionId, showErrorToast],
|
|
337
344
|
);
|
|
338
345
|
|
|
339
346
|
useEffect(() => {
|
|
@@ -458,67 +465,73 @@ export function ProxyViewerContainer({
|
|
|
458
465
|
}),
|
|
459
466
|
});
|
|
460
467
|
if (!res.ok) {
|
|
461
|
-
|
|
468
|
+
showErrorToast("Failed to clear logs");
|
|
462
469
|
return;
|
|
463
470
|
}
|
|
464
471
|
logIndexRef.current.clear();
|
|
465
472
|
setAllLogs([]);
|
|
466
|
-
|
|
473
|
+
dismissToast();
|
|
467
474
|
} catch (err) {
|
|
468
|
-
|
|
475
|
+
showErrorToast(err instanceof Error ? err.message : "Unknown error clearing logs");
|
|
469
476
|
}
|
|
470
477
|
})();
|
|
471
|
-
}, [allLogs, initialSessionId]);
|
|
472
|
-
|
|
473
|
-
const handleClearGroup = useCallback(
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
478
|
+
}, [allLogs, dismissToast, initialSessionId, showErrorToast]);
|
|
479
|
+
|
|
480
|
+
const handleClearGroup = useCallback(
|
|
481
|
+
(ids: number[]) => {
|
|
482
|
+
if (ids.length === 0) return;
|
|
483
|
+
void (async () => {
|
|
484
|
+
try {
|
|
485
|
+
const res = await fetch("/api/logs", {
|
|
486
|
+
method: "DELETE",
|
|
487
|
+
headers: { "Content-Type": "application/json" },
|
|
488
|
+
body: JSON.stringify({ ids }),
|
|
489
|
+
});
|
|
490
|
+
if (!res.ok) {
|
|
491
|
+
showErrorToast("Failed to clear group");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const idSet = new Set(ids);
|
|
495
|
+
setAllLogs((prev) => {
|
|
496
|
+
const remaining = prev.filter((l) => !idSet.has(l.id));
|
|
497
|
+
logIndexRef.current = buildLogIndex(remaining);
|
|
498
|
+
return remaining;
|
|
499
|
+
});
|
|
500
|
+
dismissToast();
|
|
501
|
+
} catch (err) {
|
|
502
|
+
showErrorToast(err instanceof Error ? err.message : "Unknown error clearing group");
|
|
485
503
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
return remaining;
|
|
491
|
-
});
|
|
492
|
-
setError(null);
|
|
493
|
-
} catch (err) {
|
|
494
|
-
setError(err instanceof Error ? err.message : "Unknown error clearing group");
|
|
495
|
-
}
|
|
496
|
-
})();
|
|
497
|
-
}, []);
|
|
498
|
-
|
|
499
|
-
const handleImportLogs = useCallback(async (file: File): Promise<ImportLogsResponse> => {
|
|
500
|
-
const formData = new FormData();
|
|
501
|
-
formData.set("file", file);
|
|
502
|
-
const result = await fetchJson("/api/logs/import", ImportLogsResponseSchema, {
|
|
503
|
-
method: "POST",
|
|
504
|
-
body: formData,
|
|
505
|
-
});
|
|
504
|
+
})();
|
|
505
|
+
},
|
|
506
|
+
[dismissToast, showErrorToast],
|
|
507
|
+
);
|
|
506
508
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
509
|
+
const handleImportLogs = useCallback(
|
|
510
|
+
async (file: File): Promise<ImportLogsResponse> => {
|
|
511
|
+
const formData = new FormData();
|
|
512
|
+
formData.set("file", file);
|
|
513
|
+
const result = await fetchJson("/api/logs/import", ImportLogsResponseSchema, {
|
|
514
|
+
method: "POST",
|
|
515
|
+
body: formData,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
setAllLogs((prev) => {
|
|
519
|
+
const importedIds = new Set(result.logs.map((log) => log.id));
|
|
520
|
+
const next = [...prev.filter((log) => !importedIds.has(log.id)), ...result.logs].sort(
|
|
521
|
+
(left, right) => left.id - right.id,
|
|
522
|
+
);
|
|
523
|
+
const trimmed = trimClientLogs(next);
|
|
524
|
+
logIndexRef.current = buildLogIndex(trimmed);
|
|
525
|
+
return trimmed;
|
|
526
|
+
});
|
|
527
|
+
setSelectedSession(result.sessionId);
|
|
528
|
+
setSelectedModel("__all__");
|
|
529
|
+
setLogPage(null);
|
|
530
|
+
dismissToast();
|
|
531
|
+
return result;
|
|
532
|
+
},
|
|
533
|
+
[dismissToast],
|
|
534
|
+
);
|
|
522
535
|
|
|
523
536
|
// Read the strip config once at the container so the virtualized list does
|
|
524
537
|
// not need N independent SWR subscriptions per row.
|
|
@@ -569,11 +582,7 @@ export function ProxyViewerContainer({
|
|
|
569
582
|
|
|
570
583
|
return (
|
|
571
584
|
<>
|
|
572
|
-
{
|
|
573
|
-
<div className="fixed top-4 right-4 bg-destructive text-destructive-foreground px-4 py-2 rounded-md text-sm z-50">
|
|
574
|
-
{error}
|
|
575
|
-
</div>
|
|
576
|
-
)}
|
|
585
|
+
<TransientToast notice={notice} onDismiss={dismissToast} />
|
|
577
586
|
<OnboardingBanner />
|
|
578
587
|
<ProxyViewer
|
|
579
588
|
logs={logs}
|
|
@@ -585,6 +594,7 @@ export function ProxyViewerContainer({
|
|
|
585
594
|
onModelChange={setSelectedModel}
|
|
586
595
|
onClearAll={handleClearAll}
|
|
587
596
|
onClearGroup={handleClearGroup}
|
|
597
|
+
onNotify={showToast}
|
|
588
598
|
onImportLogs={initialSessionId === undefined ? handleImportLogs : undefined}
|
|
589
599
|
isLoading={initialSessionId === undefined ? !streamInitialized : sessionPageLoading}
|
|
590
600
|
pagination={pagination}
|
|
@@ -5,6 +5,7 @@ import { type AlertListFilters, useAlerts, useAlertSummary } from "../../lib/use
|
|
|
5
5
|
import { cn } from "../../lib/utils";
|
|
6
6
|
import { Badge } from "../ui/badge";
|
|
7
7
|
import { Button } from "../ui/button";
|
|
8
|
+
import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
|
|
8
9
|
import {
|
|
9
10
|
Dialog,
|
|
10
11
|
DialogContent,
|
|
@@ -477,7 +478,7 @@ export function AlertsDialog(): JSX.Element {
|
|
|
477
478
|
type="button"
|
|
478
479
|
variant="ghost"
|
|
479
480
|
size="icon"
|
|
480
|
-
className=
|
|
481
|
+
className={INSPECTOR_ICON_TRIGGER_CLASS}
|
|
481
482
|
aria-label="Alerts"
|
|
482
483
|
title="Alerts"
|
|
483
484
|
>
|
|
@@ -117,7 +117,7 @@ export function ClientLogo({
|
|
|
117
117
|
return (
|
|
118
118
|
<span
|
|
119
119
|
className={cn(
|
|
120
|
-
"inline-flex size-6 shrink-0 items-center justify-center rounded-md
|
|
120
|
+
"border border-border bg-muted/40 inline-flex size-6 shrink-0 items-center justify-center rounded-md p-1 text-cyan-100",
|
|
121
121
|
className,
|
|
122
122
|
)}
|
|
123
123
|
aria-label={label}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { Button } from "../ui/button";
|
|
15
15
|
import { Badge } from "../ui/badge";
|
|
16
16
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
|
|
17
|
+
import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
|
|
17
18
|
import { cn } from "../../lib/utils";
|
|
18
19
|
import { parseJsonResponse, readApiError } from "../../lib/apiClient";
|
|
19
20
|
import {
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
import type { EvidenceReport } from "../../lib/runContract";
|
|
30
31
|
import type { SessionLogSummary } from "../../lib/sessionInfoContract";
|
|
31
32
|
import { getSessionPath } from "../../lib/sessionUrl";
|
|
33
|
+
import { ClientLogo, clientAppLabel, detectClientApp } from "../clients/ClientLogo";
|
|
32
34
|
import { useGroupEvidence } from "../../lib/useGroupEvidence";
|
|
33
35
|
import { useGroups } from "../../lib/useGroups";
|
|
34
36
|
import { useCopyFeedback } from "../proxy-viewer/useCopyFeedback";
|
|
@@ -195,6 +197,13 @@ function firstNonEmpty(...values: Array<string | null>): string {
|
|
|
195
197
|
return "-";
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
function firstOptionalText(...values: Array<string | null | undefined>): string | null {
|
|
201
|
+
for (const value of values) {
|
|
202
|
+
if (value !== null && value !== undefined && value.trim().length > 0) return value;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
198
207
|
function providerModel(member: InspectorGroupMember): string {
|
|
199
208
|
const provider = member.provider;
|
|
200
209
|
const model = member.model;
|
|
@@ -400,7 +409,7 @@ export function GroupsDialog(): JSX.Element {
|
|
|
400
409
|
type="button"
|
|
401
410
|
variant="ghost"
|
|
402
411
|
size="icon"
|
|
403
|
-
className=
|
|
412
|
+
className={INSPECTOR_ICON_TRIGGER_CLASS}
|
|
404
413
|
aria-label="Evaluation groups"
|
|
405
414
|
title="Evaluation groups"
|
|
406
415
|
>
|
|
@@ -981,6 +990,14 @@ function MemberRow({
|
|
|
981
990
|
const tokens = session?.tokenUsage.total ?? null;
|
|
982
991
|
const firstChunkMs = log?.firstChunkMs ?? null;
|
|
983
992
|
const sessionLabel = firstNonEmpty(member.label, member.sessionId);
|
|
993
|
+
const clientUserAgent = firstOptionalText(session?.clientUserAgent, log?.userAgent, member.agent);
|
|
994
|
+
const clientProjectFolder = session?.clientProjectFolder ?? null;
|
|
995
|
+
const clientApp = detectClientApp({
|
|
996
|
+
userAgent: clientUserAgent,
|
|
997
|
+
conversationId: member.sessionId,
|
|
998
|
+
clientProjectFolder,
|
|
999
|
+
});
|
|
1000
|
+
const clientLabel = clientAppLabel(clientApp);
|
|
984
1001
|
|
|
985
1002
|
return (
|
|
986
1003
|
<tr className="border-b border-border last:border-0 hover:bg-muted/20">
|
|
@@ -1003,6 +1020,7 @@ function MemberRow({
|
|
|
1003
1020
|
<ChevronRight className="size-3.5" />
|
|
1004
1021
|
)}
|
|
1005
1022
|
</Button>
|
|
1023
|
+
{clientLabel !== null && <ClientLogo client={clientApp} className="size-5" />}
|
|
1006
1024
|
{canOpenLiveSession ? (
|
|
1007
1025
|
<a
|
|
1008
1026
|
href={sessionPath}
|
|
@@ -31,7 +31,15 @@ import {
|
|
|
31
31
|
providerHasContextMetadata,
|
|
32
32
|
} from "../../lib/providerModelMetadata";
|
|
33
33
|
import { formatContextWindowTokens } from "../../lib/utils";
|
|
34
|
+
import {
|
|
35
|
+
PATH_V1_CHAT_COMPLETIONS,
|
|
36
|
+
PATH_V1_MESSAGES,
|
|
37
|
+
PATH_V1_RESPONSES,
|
|
38
|
+
previewUpstreamUrl,
|
|
39
|
+
type UpstreamUrlDiagnostic,
|
|
40
|
+
} from "../../lib/upstreamUrl";
|
|
34
41
|
import type {
|
|
42
|
+
ProviderFormatTestResults,
|
|
35
43
|
ProviderTestErrorType as ErrorType,
|
|
36
44
|
ProviderTestResult as TestResult,
|
|
37
45
|
ProviderTestResults as TestResults,
|
|
@@ -65,8 +73,6 @@ function isNotConfiguredState(result: ProviderTestState): result is { notConfigu
|
|
|
65
73
|
return Object.prototype.hasOwnProperty.call(result, "notConfigured");
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
type ProviderFormatTestResults = TestResults["anthropic"];
|
|
69
|
-
|
|
70
76
|
function hasText(value: string | undefined): value is string {
|
|
71
77
|
return value !== undefined && value.trim() !== "";
|
|
72
78
|
}
|
|
@@ -206,12 +212,12 @@ function ProviderFormatTestStatus({
|
|
|
206
212
|
results: ProviderFormatTestResults;
|
|
207
213
|
}): JSX.Element {
|
|
208
214
|
return (
|
|
209
|
-
<div className="
|
|
210
|
-
<div className="
|
|
215
|
+
<div className="grid min-w-0 gap-2 sm:grid-cols-2">
|
|
216
|
+
<div className="min-w-0 rounded-md border border-border/60 bg-muted/20 px-2 py-1.5">
|
|
211
217
|
<span className="text-[10px] text-muted-foreground">Non-stream</span>
|
|
212
218
|
<TestStatus result={results.nonStreaming} />
|
|
213
219
|
</div>
|
|
214
|
-
<div className="
|
|
220
|
+
<div className="min-w-0 rounded-md border border-border/60 bg-muted/20 px-2 py-1.5">
|
|
215
221
|
<span className="text-[10px] text-muted-foreground">Stream</span>
|
|
216
222
|
<TestStatus result={results.streaming} />
|
|
217
223
|
</div>
|
|
@@ -219,6 +225,56 @@ function ProviderFormatTestStatus({
|
|
|
219
225
|
);
|
|
220
226
|
}
|
|
221
227
|
|
|
228
|
+
function EndpointDiagnosticLine({
|
|
229
|
+
diagnostic,
|
|
230
|
+
}: {
|
|
231
|
+
diagnostic: UpstreamUrlDiagnostic;
|
|
232
|
+
}): JSX.Element {
|
|
233
|
+
return (
|
|
234
|
+
<div className={diagnostic.severity === "warning" ? "text-amber-200" : "text-muted-foreground"}>
|
|
235
|
+
{diagnostic.message}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function ProviderEndpointSummary({
|
|
241
|
+
label,
|
|
242
|
+
baseUrl,
|
|
243
|
+
endpoint,
|
|
244
|
+
providerName,
|
|
245
|
+
children,
|
|
246
|
+
}: {
|
|
247
|
+
label: string;
|
|
248
|
+
baseUrl: string;
|
|
249
|
+
endpoint: string;
|
|
250
|
+
providerName: string;
|
|
251
|
+
children?: ReactNode;
|
|
252
|
+
}): JSX.Element {
|
|
253
|
+
const preview = previewUpstreamUrl(baseUrl, endpoint, { providerName });
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<div className="flex min-w-0 flex-col gap-2 border-t border-border/60 pt-2 first:border-t-0 first:pt-0">
|
|
257
|
+
<div className="grid min-w-0 gap-1 text-xs sm:grid-cols-[104px_minmax(0,1fr)]">
|
|
258
|
+
<span className="font-medium text-muted-foreground">{label}</span>
|
|
259
|
+
<code className="min-w-0 break-all font-mono text-foreground/80">{baseUrl}</code>
|
|
260
|
+
<span className="text-[10px] uppercase text-muted-foreground/80">Final</span>
|
|
261
|
+
<code className="min-w-0 break-all font-mono text-foreground">{preview.finalUrl}</code>
|
|
262
|
+
</div>
|
|
263
|
+
{preview.diagnostics.length > 0 && (
|
|
264
|
+
<div className="space-y-1 text-xs">
|
|
265
|
+
{preview.diagnostics.map((diagnostic) => (
|
|
266
|
+
<EndpointDiagnosticLine
|
|
267
|
+
key={`${diagnostic.severity}:${diagnostic.message}`}
|
|
268
|
+
diagnostic={diagnostic}
|
|
269
|
+
/>
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
{children}
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
222
278
|
function getErrorIcon(type: ErrorType): JSX.Element {
|
|
223
279
|
const iconProps = { className: "size-3", strokeWidth: 2 };
|
|
224
280
|
switch (type) {
|
|
@@ -314,8 +370,8 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
314
370
|
<TooltipProvider>
|
|
315
371
|
<Tooltip>
|
|
316
372
|
<TooltipTrigger asChild>
|
|
317
|
-
<div className="flex items-center gap-1 text-xs text-
|
|
318
|
-
<CheckCircle className="size-3" />
|
|
373
|
+
<div className="flex items-center gap-1 text-xs text-cyan-100 shrink-0">
|
|
374
|
+
<CheckCircle className="size-3 text-cyan-300" />
|
|
319
375
|
<span>Connected</span>
|
|
320
376
|
{tokenParts.length > 0 && (
|
|
321
377
|
<span className="text-muted-foreground">({displayTokens})</span>
|
|
@@ -440,15 +496,40 @@ export function ProviderCard({
|
|
|
440
496
|
const canRefreshMetadata =
|
|
441
497
|
provider.modelMetadataUrl !== undefined && provider.modelMetadataUrl.trim() !== "";
|
|
442
498
|
const metadataImportLabel = canRefreshMetadata ? "Refresh" : "Import defaults";
|
|
499
|
+
const providerSourceBadge =
|
|
500
|
+
provider.source === "company" ? (
|
|
501
|
+
<TooltipProvider>
|
|
502
|
+
<Tooltip>
|
|
503
|
+
<TooltipTrigger asChild>
|
|
504
|
+
<span className="shrink-0 rounded border border-cyan-300/20 bg-cyan-300/[0.08] px-1.5 py-0.5 text-xs text-cyan-100">
|
|
505
|
+
Company
|
|
506
|
+
</span>
|
|
507
|
+
</TooltipTrigger>
|
|
508
|
+
<TooltipContent>Company-provided API key</TooltipContent>
|
|
509
|
+
</Tooltip>
|
|
510
|
+
</TooltipProvider>
|
|
511
|
+
) : provider.source === "personal" ? (
|
|
512
|
+
<TooltipProvider>
|
|
513
|
+
<Tooltip>
|
|
514
|
+
<TooltipTrigger asChild>
|
|
515
|
+
<span className="shrink-0 rounded border border-violet-300/20 bg-violet-300/[0.08] px-1.5 py-0.5 text-xs text-violet-100">
|
|
516
|
+
Personal
|
|
517
|
+
</span>
|
|
518
|
+
</TooltipTrigger>
|
|
519
|
+
<TooltipContent>Your personal API key</TooltipContent>
|
|
520
|
+
</Tooltip>
|
|
521
|
+
</TooltipProvider>
|
|
522
|
+
) : null;
|
|
443
523
|
|
|
444
524
|
return (
|
|
445
525
|
<div
|
|
446
|
-
className={`border
|
|
526
|
+
className={`border border-border bg-card shadow-sm flex flex-col gap-3 rounded-[8px] p-4 transition-all duration-500 ${isHighlighted === true ? "ring-2 ring-primary shadow-md" : ""}`}
|
|
447
527
|
>
|
|
448
528
|
<div className="flex items-start justify-between gap-2">
|
|
449
529
|
<div className="flex items-center gap-2 min-w-0">
|
|
450
530
|
<span className="font-medium truncate">{provider.name}</span>
|
|
451
|
-
{
|
|
531
|
+
{providerSourceBadge}
|
|
532
|
+
{providerSourceBadge === null && provider.source === "company" && (
|
|
452
533
|
<TooltipProvider>
|
|
453
534
|
<Tooltip>
|
|
454
535
|
<TooltipTrigger asChild>
|
|
@@ -460,7 +541,7 @@ export function ProviderCard({
|
|
|
460
541
|
</Tooltip>
|
|
461
542
|
</TooltipProvider>
|
|
462
543
|
)}
|
|
463
|
-
{provider.source === "personal" && (
|
|
544
|
+
{providerSourceBadge === null && provider.source === "personal" && (
|
|
464
545
|
<TooltipProvider>
|
|
465
546
|
<Tooltip>
|
|
466
547
|
<TooltipTrigger asChild>
|
|
@@ -575,39 +656,47 @@ export function ProviderCard({
|
|
|
575
656
|
className="text-muted-foreground hover:text-foreground transition-colors p-1"
|
|
576
657
|
aria-label="Copy API key"
|
|
577
658
|
>
|
|
578
|
-
{copied ? <Check className="size-4 text-
|
|
659
|
+
{copied ? <Check className="size-4 text-cyan-300" /> : <Copy className="size-4" />}
|
|
579
660
|
</button>
|
|
580
661
|
</div>
|
|
581
662
|
|
|
582
663
|
{provider.anthropicBaseUrl !== undefined && provider.anthropicBaseUrl !== "" && (
|
|
583
|
-
<
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
664
|
+
<ProviderEndpointSummary
|
|
665
|
+
label="Anthropic"
|
|
666
|
+
baseUrl={provider.anthropicBaseUrl}
|
|
667
|
+
endpoint={PATH_V1_MESSAGES}
|
|
668
|
+
providerName={provider.name}
|
|
669
|
+
>
|
|
588
670
|
{testResults !== undefined && (
|
|
589
671
|
<ProviderFormatTestStatus results={testResults.anthropic} />
|
|
590
672
|
)}
|
|
591
|
-
</
|
|
673
|
+
</ProviderEndpointSummary>
|
|
592
674
|
)}
|
|
593
675
|
|
|
594
676
|
{provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
|
|
595
|
-
<
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
677
|
+
<ProviderEndpointSummary
|
|
678
|
+
label="OpenAI Chat"
|
|
679
|
+
baseUrl={provider.openaiBaseUrl}
|
|
680
|
+
endpoint={PATH_V1_CHAT_COMPLETIONS}
|
|
681
|
+
providerName={provider.name}
|
|
682
|
+
>
|
|
683
|
+
{testResults !== undefined && (
|
|
684
|
+
<ProviderFormatTestStatus results={testResults.openaiChat} />
|
|
685
|
+
)}
|
|
686
|
+
</ProviderEndpointSummary>
|
|
602
687
|
)}
|
|
603
688
|
|
|
604
689
|
{provider.openaiResponsesBaseUrl !== undefined && provider.openaiResponsesBaseUrl !== "" && (
|
|
605
|
-
<
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
690
|
+
<ProviderEndpointSummary
|
|
691
|
+
label="OpenAI Responses"
|
|
692
|
+
baseUrl={provider.openaiResponsesBaseUrl}
|
|
693
|
+
endpoint={PATH_V1_RESPONSES}
|
|
694
|
+
providerName={provider.name}
|
|
695
|
+
>
|
|
696
|
+
{testResults !== undefined && (
|
|
697
|
+
<ProviderFormatTestStatus results={testResults.openaiResponses} />
|
|
698
|
+
)}
|
|
699
|
+
</ProviderEndpointSummary>
|
|
611
700
|
)}
|
|
612
701
|
|
|
613
702
|
{testResults?.testedAt !== undefined && (
|
|
@@ -644,9 +733,15 @@ export function ProviderCard({
|
|
|
644
733
|
)}
|
|
645
734
|
{provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
|
|
646
735
|
<th className="text-left py-1 px-2 font-medium text-muted-foreground">
|
|
647
|
-
|
|
736
|
+
Chat
|
|
648
737
|
</th>
|
|
649
738
|
)}
|
|
739
|
+
{provider.openaiResponsesBaseUrl !== undefined &&
|
|
740
|
+
provider.openaiResponsesBaseUrl !== "" && (
|
|
741
|
+
<th className="text-left py-1 px-2 font-medium text-muted-foreground">
|
|
742
|
+
Responses
|
|
743
|
+
</th>
|
|
744
|
+
)}
|
|
650
745
|
</tr>
|
|
651
746
|
</thead>
|
|
652
747
|
<tbody>
|
|
@@ -661,9 +756,15 @@ export function ProviderCard({
|
|
|
661
756
|
)}
|
|
662
757
|
{provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
|
|
663
758
|
<td className="py-1 px-2 align-top">
|
|
664
|
-
<ProviderFormatTestStatus results={modelResult.
|
|
759
|
+
<ProviderFormatTestStatus results={modelResult.openaiChat} />
|
|
665
760
|
</td>
|
|
666
761
|
)}
|
|
762
|
+
{provider.openaiResponsesBaseUrl !== undefined &&
|
|
763
|
+
provider.openaiResponsesBaseUrl !== "" && (
|
|
764
|
+
<td className="py-1 px-2 align-top">
|
|
765
|
+
<ProviderFormatTestStatus results={modelResult.openaiResponses} />
|
|
766
|
+
</td>
|
|
767
|
+
)}
|
|
667
768
|
</tr>
|
|
668
769
|
))}
|
|
669
770
|
</tbody>
|