@tonyclaw/agent-inspector 2.1.16 → 2.1.18
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-Ccrrcx1j.js → CompareDrawer-Fqqw5Lci.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CP_RzwW9.js +106 -0
- package/.output/public/assets/{ReplayDialog-C7axhr-l.js → ReplayDialog-hYhz_i6W.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-WOptg8j_.js → RequestAnatomy-CTuWbtwC.js} +1 -1
- package/.output/public/assets/{ResponseView-CSAcxh_M.js → ResponseView-CsZ7S5Gv.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-DJS5KhPx.js → StreamingChunkSequence-PIbJMotU.js} +1 -1
- package/.output/public/assets/_sessionId-CUGA9qU3.js +1 -0
- package/.output/public/assets/{index-DMPNh46t.js → index-F7mynwuH.js} +1 -1
- package/.output/public/assets/index-G4dTUobw.js +1 -0
- package/.output/public/assets/index-gvYkgMPF.css +1 -0
- package/.output/public/assets/{json-viewer-inX5QSa3.js → json-viewer-DCXtNmmD.js} +1 -1
- package/.output/public/assets/{main-aCAKYGSD.js → main-BJZf7_Ib.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +220 -202
- package/.output/server/{_sessionId-C0fhvQnn.mjs → _sessionId-DjSHDNh8.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-CO-Ti5sg.mjs → CompareDrawer-Dal52IFo.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-N4J7uBCX.mjs → ProxyViewerContainer-CVqFZGff.mjs} +1833 -1366
- package/.output/server/_ssr/{ReplayDialog-J1VxC0In.mjs → ReplayDialog-BOEgdMes.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-DBH-F3rq.mjs → RequestAnatomy-gch7_vCV.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-DLFwV7Hd.mjs → ResponseView-lEWPy-H4.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-BywmuND0.mjs → StreamingChunkSequence-DnudbJ1O.mjs} +2 -2
- package/.output/server/_ssr/{index-CStymIc0.mjs → index-Cza8zDrW.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-DlQu0N1x.mjs → json-viewer-C2lIVEX3.mjs} +3 -3
- package/.output/server/_ssr/{router-CQ4Zxhmr.mjs → router-bF9uzd1k.mjs} +939 -113
- package/.output/server/{_tanstack-start-manifest_v-BHeJBGfo.mjs → _tanstack-start-manifest_v-ErvW-mtr.mjs} +1 -1
- package/.output/server/index.mjs +74 -74
- 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 +395 -64
- package/src/components/providers/ProviderCard.tsx +132 -31
- package/src/components/providers/ProviderForm.tsx +43 -11
- package/src/components/providers/ProvidersPanel.tsx +1 -1
- package/src/components/proxy-viewer/ConversationHeader.tsx +133 -112
- package/src/components/proxy-viewer/LogEntry.tsx +109 -26
- package/src/components/proxy-viewer/LogEntryHeader.tsx +339 -329
- package/src/components/proxy-viewer/ThreadConnector.tsx +49 -46
- package/src/components/proxy-viewer/TurnGroup.tsx +22 -62
- package/src/components/proxy-viewer/bodyHydration.ts +56 -0
- package/src/lib/providerTestContract.ts +56 -10
- package/src/lib/providerTestPrompt.ts +23 -1
- package/src/lib/upstreamUrl.ts +105 -1
- package/src/mcp/server.ts +275 -0
- package/src/mcp/toolHandlers.ts +381 -4
- package/src/proxy/logSearch.ts +3 -0
- package/src/proxy/store.ts +40 -15
- package/src/routes/api/logs.ts +8 -2
- package/src/routes/api/providers.$providerId.test.log.ts +85 -10
- package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +0 -106
- package/.output/public/assets/_sessionId-DUSalzKH.js +0 -1
- package/.output/public/assets/index-BPpA21dY.css +0 -1
- package/.output/public/assets/index-Bt0Az2I2.js +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type JSX, useMemo } from "react";
|
|
2
|
-
import { cn } from "../../lib/utils";
|
|
1
|
+
import { type JSX, type KeyboardEvent, type MouseEvent, useMemo } from "react";
|
|
3
2
|
import { isTurnBoundary, type StopReason } from "../../lib/stopReason";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
4
|
import { getCrabVariant, getInteriorCrabVariant } from "../ui/crab-variants";
|
|
5
5
|
|
|
6
6
|
export type ThreadConnectorProps = {
|
|
@@ -32,13 +32,10 @@ function boundaryTitle(stopReason: StopReason): string {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* root font-size. The bottom spacer is flex-1 so the outgoing
|
|
40
|
-
* line adapts to whatever height the LogEntry occupies.
|
|
41
|
-
*/
|
|
35
|
+
function crabShellClass(interactive: boolean): string {
|
|
36
|
+
return interactive ? "cursor-pointer transition-transform hover:scale-110" : "";
|
|
37
|
+
}
|
|
38
|
+
|
|
42
39
|
export function ThreadConnector({
|
|
43
40
|
stopReason,
|
|
44
41
|
isPending,
|
|
@@ -52,53 +49,55 @@ export function ThreadConnector({
|
|
|
52
49
|
const isBoundary = isTurnBoundary(stopReason);
|
|
53
50
|
const isFusedBoundary = isOnlyEntry && isTurnStart && isBoundary;
|
|
54
51
|
const isRunning = isPending && !isBoundary;
|
|
52
|
+
const interactive = collapsible && onToggle !== undefined;
|
|
55
53
|
const Crab = useMemo(() => getCrabVariant(crabIndex), [crabIndex]);
|
|
56
54
|
const FusedCrab = useMemo(() => getInteriorCrabVariant(crabIndex), [crabIndex]);
|
|
57
55
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
e.preventDefault();
|
|
71
|
-
onToggle();
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
} as const)
|
|
75
|
-
: ({} as const);
|
|
56
|
+
const handleCrabClick = (event: MouseEvent<HTMLSpanElement>): void => {
|
|
57
|
+
if (!interactive || onToggle === undefined) return;
|
|
58
|
+
event.stopPropagation();
|
|
59
|
+
onToggle();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleCrabKeyDown = (event: KeyboardEvent<HTMLSpanElement>): void => {
|
|
63
|
+
if (!interactive || onToggle === undefined) return;
|
|
64
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
onToggle();
|
|
67
|
+
};
|
|
76
68
|
|
|
77
69
|
return (
|
|
78
70
|
<div className="flex flex-col items-center w-6 shrink-0 pt-0.5 pb-0.5">
|
|
79
|
-
{/* Top spacer — crab centre must land on the LogEntry index midline.
|
|
80
|
-
* Index midline from container top = border(1px) + py-1(0.25rem) + ½line(0.5rem)
|
|
81
|
-
* = 1px + 0.75rem. Crab centre = pt(2px) + spacer + 7px(half crab).
|
|
82
|
-
* ∴ spacer = 0.75rem - 8px. (at 16px root: 12-8=4px; 2+4+7=13px). */}
|
|
83
71
|
<div className="flex justify-center h-[calc(0.75rem-8px)]">
|
|
84
72
|
{!isFirst && <div className="w-0.5 bg-muted-foreground/30 h-full" />}
|
|
85
73
|
</div>
|
|
86
74
|
|
|
87
|
-
{/* Crab — pinned to the fixed offset above so it never drifts
|
|
88
|
-
* when the LogEntry expands / collapses. Clickable when the
|
|
89
|
-
* turn is collapsible (replaces the old chevron toggle). */}
|
|
90
75
|
{isFusedBoundary ? (
|
|
91
|
-
<span
|
|
76
|
+
<span
|
|
77
|
+
role={interactive ? "button" : undefined}
|
|
78
|
+
tabIndex={interactive ? 0 : undefined}
|
|
79
|
+
title="Single-request turn"
|
|
80
|
+
className={crabShellClass(interactive)}
|
|
81
|
+
onClick={handleCrabClick}
|
|
82
|
+
onKeyDown={handleCrabKeyDown}
|
|
83
|
+
>
|
|
92
84
|
<FusedCrab
|
|
93
85
|
className={cn(
|
|
94
|
-
"size-3.5 text-
|
|
86
|
+
"size-3.5 text-[#80FF00]",
|
|
95
87
|
"animate-crab-settle",
|
|
96
|
-
"drop-shadow-[0_0_4px_rgba(
|
|
88
|
+
"drop-shadow-[0_0_4px_rgba(128,255,0,0.5)]",
|
|
97
89
|
)}
|
|
98
90
|
/>
|
|
99
91
|
</span>
|
|
100
92
|
) : isBoundary ? (
|
|
101
|
-
<span
|
|
93
|
+
<span
|
|
94
|
+
role={interactive ? "button" : undefined}
|
|
95
|
+
tabIndex={interactive ? 0 : undefined}
|
|
96
|
+
title={boundaryTitle(stopReason)}
|
|
97
|
+
className={crabShellClass(interactive)}
|
|
98
|
+
onClick={handleCrabClick}
|
|
99
|
+
onKeyDown={handleCrabKeyDown}
|
|
100
|
+
>
|
|
102
101
|
<Crab
|
|
103
102
|
className={cn(
|
|
104
103
|
"size-3.5 text-amber-400",
|
|
@@ -108,28 +107,32 @@ export function ThreadConnector({
|
|
|
108
107
|
/>
|
|
109
108
|
</span>
|
|
110
109
|
) : isTurnStart ? (
|
|
111
|
-
<span
|
|
110
|
+
<span
|
|
111
|
+
role={interactive ? "button" : undefined}
|
|
112
|
+
tabIndex={interactive ? 0 : undefined}
|
|
113
|
+
title="Start of turn"
|
|
114
|
+
className={crabShellClass(interactive)}
|
|
115
|
+
onClick={handleCrabClick}
|
|
116
|
+
onKeyDown={handleCrabKeyDown}
|
|
117
|
+
>
|
|
112
118
|
<Crab
|
|
113
119
|
className={cn(
|
|
114
|
-
"size-3.5 text-
|
|
120
|
+
"size-3.5 text-emerald-400",
|
|
115
121
|
"animate-crab-appear",
|
|
116
|
-
"drop-shadow-[0_0_4px_rgba(
|
|
122
|
+
"drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
|
|
117
123
|
)}
|
|
118
124
|
/>
|
|
119
125
|
</span>
|
|
120
126
|
) : isRunning ? (
|
|
121
|
-
<span title="Processing
|
|
127
|
+
<span title="Processing">
|
|
122
128
|
<Crab className={cn("size-3.5 text-amber-300/80", "animate-crab-crawl")} />
|
|
123
129
|
</span>
|
|
124
130
|
) : (
|
|
125
|
-
<span>
|
|
131
|
+
<span title="Request in turn">
|
|
126
132
|
<Crab className="size-3.5 text-muted-foreground/40" />
|
|
127
133
|
</span>
|
|
128
134
|
)}
|
|
129
135
|
|
|
130
|
-
{/* Bottom spacer — flex-1 fills whatever height the LogEntry
|
|
131
|
-
* consumes (header + expanded content). Turn boundaries have
|
|
132
|
-
* no outgoing line. */}
|
|
133
136
|
<div className="flex-1 flex justify-center min-h-0">
|
|
134
137
|
{!isBoundary && (
|
|
135
138
|
<div
|
|
@@ -13,9 +13,7 @@ import { isTurnBoundary } from "../../lib/stopReason";
|
|
|
13
13
|
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
14
14
|
import { cn, formatTokens } from "../../lib/utils";
|
|
15
15
|
import type { CapturedLog } from "../../contracts";
|
|
16
|
-
import { getCrabVariant } from "../ui/crab-variants";
|
|
17
16
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
|
|
18
|
-
import { ROW_CHEVRON_SLOT_CLASS } from "./ProviderLogoStack";
|
|
19
17
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
20
18
|
import { LogEntry } from "./LogEntry";
|
|
21
19
|
import {
|
|
@@ -145,10 +143,6 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
145
143
|
};
|
|
146
144
|
}, [entries, lastIdx]);
|
|
147
145
|
|
|
148
|
-
// Crab variant creators for the dual-crab collapsed layout
|
|
149
|
-
const StartCrab = useMemo(() => getCrabVariant(entries[0]?.log.id ?? 0), [entries]);
|
|
150
|
-
const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
|
|
151
|
-
|
|
152
146
|
const bgClass = turnIndex % 2 === 0 ? "bg-white/[0.012]" : "bg-cyan-300/[0.012]";
|
|
153
147
|
const aggregateIsSlow =
|
|
154
148
|
aggregate.maxElapsed !== null &&
|
|
@@ -209,7 +203,6 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
209
203
|
)}
|
|
210
204
|
>
|
|
211
205
|
{collapsed ? (
|
|
212
|
-
/* ---- Collapsed: dual-crab (+ summary card for multi-log turns) ---- */
|
|
213
206
|
<div
|
|
214
207
|
data-nav-id={`turn-collapsed-${String(firstLogId)}`}
|
|
215
208
|
data-nav-action="expand"
|
|
@@ -231,78 +224,38 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
231
224
|
</span>
|
|
232
225
|
</div>
|
|
233
226
|
|
|
234
|
-
|
|
235
|
-
<div className="w-6 shrink-0 flex flex-col items-center pt-0.5 pb-0.5">
|
|
227
|
+
<div className="w-6 shrink-0 flex flex-col items-center pt-1.5 pb-1.5">
|
|
236
228
|
<div className="flex justify-center h-[calc(0.75rem-8px)]" />
|
|
237
|
-
<span
|
|
238
|
-
role="button"
|
|
239
|
-
tabIndex={0}
|
|
240
|
-
title="Start of turn - click to expand"
|
|
241
|
-
className="cursor-pointer"
|
|
242
|
-
onClick={(e) => {
|
|
243
|
-
e.stopPropagation();
|
|
244
|
-
toggleCollapse();
|
|
245
|
-
}}
|
|
246
|
-
onKeyDown={(e) => {
|
|
247
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
248
|
-
e.preventDefault();
|
|
249
|
-
toggleCollapse();
|
|
250
|
-
}
|
|
251
|
-
}}
|
|
252
|
-
>
|
|
253
|
-
<StartCrab
|
|
254
|
-
className={cn(
|
|
255
|
-
"size-3.5 text-cyan-300/85",
|
|
256
|
-
"animate-crab-appear drop-shadow-[0_0_4px_rgba(103,232,249,0.32)]",
|
|
257
|
-
)}
|
|
258
|
-
/>
|
|
259
|
-
</span>
|
|
260
|
-
|
|
229
|
+
<span className="size-2 rounded-full border border-cyan-300/45 bg-cyan-300/20" />
|
|
261
230
|
<div className="flex-1 flex justify-center min-h-0">
|
|
262
|
-
<div className="w-
|
|
231
|
+
<div className="w-px bg-muted-foreground/25 h-full" />
|
|
263
232
|
</div>
|
|
264
|
-
|
|
265
233
|
<span
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}}
|
|
274
|
-
onKeyDown={(e) => {
|
|
275
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
276
|
-
e.preventDefault();
|
|
277
|
-
toggleCollapse();
|
|
278
|
-
}
|
|
279
|
-
}}
|
|
280
|
-
>
|
|
281
|
-
<EndCrab
|
|
282
|
-
className={cn(
|
|
283
|
-
"size-3.5 text-amber-400",
|
|
284
|
-
"animate-crab-settle drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]",
|
|
285
|
-
)}
|
|
286
|
-
/>
|
|
287
|
-
</span>
|
|
234
|
+
className={cn(
|
|
235
|
+
"size-2 rounded-full border",
|
|
236
|
+
isComplete
|
|
237
|
+
? "border-amber-300/45 bg-amber-300/20"
|
|
238
|
+
: "border-muted-foreground/30 bg-muted/30",
|
|
239
|
+
)}
|
|
240
|
+
/>
|
|
288
241
|
</div>
|
|
289
242
|
|
|
290
243
|
{/* Summary content - hidden for single-log turns. */}
|
|
291
244
|
{entries.length > 1 && (
|
|
292
245
|
<div
|
|
293
246
|
className={cn(
|
|
294
|
-
"flex-1 min-w-0 mb-0.5 rounded-
|
|
295
|
-
"border border-border bg-card shadow-sm",
|
|
247
|
+
"flex-1 min-w-0 mb-0.5 rounded-[8px] py-1.5 px-3 flex items-center gap-3 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
|
|
248
|
+
"border border-border bg-card shadow-sm transition-colors hover:border-cyan-200/20",
|
|
296
249
|
bgClass,
|
|
297
250
|
)}
|
|
298
251
|
>
|
|
299
|
-
<span className=
|
|
300
|
-
|
|
252
|
+
<span className="font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/70">
|
|
253
|
+
{turnLabel}
|
|
301
254
|
</span>
|
|
302
255
|
|
|
303
256
|
{/* ID range */}
|
|
304
257
|
<span
|
|
305
|
-
className="text-blue-
|
|
258
|
+
className="text-blue-300/85 font-mono font-semibold tabular-nums shrink-0"
|
|
306
259
|
title={`Log IDs ${String(entries[0]?.log.id ?? "?")} ~ ${String(
|
|
307
260
|
entries[lastIdx]?.log.id ?? "?",
|
|
308
261
|
)}`}
|
|
@@ -363,6 +316,13 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
363
316
|
|
|
364
317
|
{/* Spacer */}
|
|
365
318
|
<span className="flex-1 min-w-0" />
|
|
319
|
+
|
|
320
|
+
<span
|
|
321
|
+
className="border border-input bg-background inline-flex size-7 shrink-0 items-center justify-center rounded-md text-slate-200"
|
|
322
|
+
aria-hidden="true"
|
|
323
|
+
>
|
|
324
|
+
<ChevronRight className="size-4 shrink-0" />
|
|
325
|
+
</span>
|
|
366
326
|
</div>
|
|
367
327
|
)}
|
|
368
328
|
</div>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ApiTimeoutError } from "../../lib/apiClient";
|
|
2
|
+
|
|
3
|
+
export type FullBodyHydrationError = {
|
|
4
|
+
title: string;
|
|
5
|
+
detail: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL =
|
|
9
|
+
"The compact log metadata is still available. Retry if the local server was restarting or the body store was briefly unavailable.";
|
|
10
|
+
|
|
11
|
+
export function formatBytes(bytes: number): string {
|
|
12
|
+
if (bytes < 1024) return `${bytes.toLocaleString()} B`;
|
|
13
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
14
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
15
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GiB`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatBodyHydrationFootprint(
|
|
19
|
+
requestBytes: number | null | undefined,
|
|
20
|
+
responseBytes: number | null | undefined,
|
|
21
|
+
): string | null {
|
|
22
|
+
const parts: string[] = [];
|
|
23
|
+
|
|
24
|
+
if (requestBytes !== null && requestBytes !== undefined) {
|
|
25
|
+
parts.push(`Request ${formatBytes(requestBytes)}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (responseBytes !== null && responseBytes !== undefined) {
|
|
29
|
+
parts.push(`Response ${formatBytes(responseBytes)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return parts.length === 0 ? null : parts.join(" · ");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function describeFullBodyHydrationError(error: unknown): FullBodyHydrationError {
|
|
36
|
+
if (error instanceof ApiTimeoutError) {
|
|
37
|
+
return {
|
|
38
|
+
title: "Full bodies timed out",
|
|
39
|
+
detail:
|
|
40
|
+
"The log store did not answer before the UI timeout. Compact metadata is still available; retry loading the bodies.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (error instanceof Error) {
|
|
45
|
+
const message = error.message.trim();
|
|
46
|
+
return {
|
|
47
|
+
title: "Full bodies could not be loaded",
|
|
48
|
+
detail: message === "" ? DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL : message,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
title: "Full bodies could not be loaded",
|
|
54
|
+
detail: DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -72,22 +72,58 @@ const ProviderFormatTestResultsSchema = z.object({
|
|
|
72
72
|
streaming: ProviderTestStateSchema,
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
anthropic: ProviderFormatTestResultsSchema,
|
|
77
|
-
openai: ProviderFormatTestResultsSchema,
|
|
78
|
-
});
|
|
75
|
+
export type ProviderFormatTestResults = z.infer<typeof ProviderFormatTestResultsSchema>;
|
|
79
76
|
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
77
|
+
export function createNotConfiguredProviderFormatTestResults(): ProviderFormatTestResults {
|
|
78
|
+
return {
|
|
79
|
+
nonStreaming: { notConfigured: true },
|
|
80
|
+
streaming: { notConfigured: true },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ModelTestResultsSchema = z
|
|
85
|
+
.object({
|
|
86
|
+
anthropic: ProviderFormatTestResultsSchema,
|
|
87
|
+
openai: ProviderFormatTestResultsSchema.optional(),
|
|
88
|
+
openaiChat: ProviderFormatTestResultsSchema.optional(),
|
|
89
|
+
openaiResponses: ProviderFormatTestResultsSchema.optional(),
|
|
90
|
+
})
|
|
91
|
+
.transform((value) => {
|
|
92
|
+
const openaiChat =
|
|
93
|
+
value.openaiChat ?? value.openai ?? createNotConfiguredProviderFormatTestResults();
|
|
94
|
+
return {
|
|
95
|
+
...value,
|
|
96
|
+
openai: value.openai ?? openaiChat,
|
|
97
|
+
openaiChat,
|
|
98
|
+
openaiResponses: value.openaiResponses ?? createNotConfiguredProviderFormatTestResults(),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const ProviderTestResultsSchema = z
|
|
103
|
+
.object({
|
|
104
|
+
anthropic: ProviderFormatTestResultsSchema,
|
|
105
|
+
openai: ProviderFormatTestResultsSchema.optional(),
|
|
106
|
+
openaiChat: ProviderFormatTestResultsSchema.optional(),
|
|
107
|
+
openaiResponses: ProviderFormatTestResultsSchema.optional(),
|
|
108
|
+
models: z.record(z.string(), ModelTestResultsSchema).optional(),
|
|
109
|
+
testedAt: z.string().optional(),
|
|
110
|
+
})
|
|
111
|
+
.transform((value) => {
|
|
112
|
+
const openaiChat =
|
|
113
|
+
value.openaiChat ?? value.openai ?? createNotConfiguredProviderFormatTestResults();
|
|
114
|
+
return {
|
|
115
|
+
...value,
|
|
116
|
+
openai: value.openai ?? openaiChat,
|
|
117
|
+
openaiChat,
|
|
118
|
+
openaiResponses: value.openaiResponses ?? createNotConfiguredProviderFormatTestResults(),
|
|
119
|
+
};
|
|
120
|
+
});
|
|
86
121
|
|
|
87
122
|
export type ProviderTestErrorType = z.infer<typeof ProviderTestErrorTypeSchema>;
|
|
88
123
|
export type ProviderTestDebug = z.infer<typeof ProviderTestDebugSchema>;
|
|
89
124
|
export type ProviderTestResult = z.infer<typeof ProviderTestResultSchema>;
|
|
90
125
|
export type ProviderTestState = z.infer<typeof ProviderTestStateSchema>;
|
|
126
|
+
export type ProviderModelTestResults = z.infer<typeof ModelTestResultsSchema>;
|
|
91
127
|
export type ProviderTestResults = z.infer<typeof ProviderTestResultsSchema>;
|
|
92
128
|
|
|
93
129
|
export function createPendingProviderTestResults(): ProviderTestResults {
|
|
@@ -100,6 +136,14 @@ export function createPendingProviderTestResults(): ProviderTestResults {
|
|
|
100
136
|
nonStreaming: { testing: true },
|
|
101
137
|
streaming: { testing: true },
|
|
102
138
|
},
|
|
139
|
+
openaiChat: {
|
|
140
|
+
nonStreaming: { testing: true },
|
|
141
|
+
streaming: { testing: true },
|
|
142
|
+
},
|
|
143
|
+
openaiResponses: {
|
|
144
|
+
nonStreaming: { testing: true },
|
|
145
|
+
streaming: { testing: true },
|
|
146
|
+
},
|
|
103
147
|
};
|
|
104
148
|
}
|
|
105
149
|
|
|
@@ -118,5 +162,7 @@ export function createFailedProviderTestResults(
|
|
|
118
162
|
return {
|
|
119
163
|
anthropic: createFormatResult(),
|
|
120
164
|
openai: createFormatResult(),
|
|
165
|
+
openaiChat: createFormatResult(),
|
|
166
|
+
openaiResponses: createFormatResult(),
|
|
121
167
|
};
|
|
122
168
|
}
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
export type ProviderTestMode = "non-streaming" | "streaming";
|
|
2
|
+
export type ProviderTestApiShape = "chat" | "responses";
|
|
2
3
|
|
|
3
4
|
export type ProviderTestMessage = {
|
|
4
5
|
role: "user";
|
|
5
6
|
content: string;
|
|
6
7
|
};
|
|
7
8
|
|
|
8
|
-
export type
|
|
9
|
+
export type ProviderTestChatRequestBody = {
|
|
9
10
|
model: string;
|
|
10
11
|
messages: ProviderTestMessage[];
|
|
11
12
|
max_tokens: number;
|
|
12
13
|
stream: boolean;
|
|
13
14
|
};
|
|
14
15
|
|
|
16
|
+
export type ProviderTestResponsesRequestBody = {
|
|
17
|
+
model: string;
|
|
18
|
+
input: string;
|
|
19
|
+
max_output_tokens: number;
|
|
20
|
+
stream: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ProviderTestRequestBody =
|
|
24
|
+
| ProviderTestChatRequestBody
|
|
25
|
+
| ProviderTestResponsesRequestBody;
|
|
26
|
+
|
|
15
27
|
const MEMORY_PROBE_FACTS = [
|
|
16
28
|
"Project fact: Agent Inspector captures LLM traffic and turns it into Raw Trace, Session Episode, and Memory Candidate layers.",
|
|
17
29
|
"Procedure: before release, run bun test, npm run typecheck, npm run lint, npm run build, and openspec validate --all --strict.",
|
|
@@ -63,7 +75,17 @@ export function buildProviderTestMessages(mode: ProviderTestMode): ProviderTestM
|
|
|
63
75
|
export function buildProviderTestRequestBody(
|
|
64
76
|
model: string,
|
|
65
77
|
mode: ProviderTestMode,
|
|
78
|
+
apiShape: ProviderTestApiShape = "chat",
|
|
66
79
|
): ProviderTestRequestBody {
|
|
80
|
+
if (apiShape === "responses") {
|
|
81
|
+
return {
|
|
82
|
+
model,
|
|
83
|
+
input: buildProviderTestUserPrompt(mode),
|
|
84
|
+
max_output_tokens: maxTokensForMode(mode),
|
|
85
|
+
stream: mode === "streaming",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
const base = {
|
|
68
90
|
model,
|
|
69
91
|
messages: buildProviderTestMessages(mode),
|
package/src/lib/upstreamUrl.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
export const PATH_V1_CHAT_COMPLETIONS = "/v1/chat/completions";
|
|
2
2
|
export const PATH_V1_RESPONSES = "/v1/responses";
|
|
3
|
+
export const PATH_V1_MESSAGES = "/v1/messages";
|
|
4
|
+
|
|
5
|
+
export type UpstreamUrlDiagnostic = {
|
|
6
|
+
severity: "info" | "warning";
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type UpstreamUrlPreviewOptions = {
|
|
11
|
+
providerName?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type UpstreamUrlPreview = {
|
|
15
|
+
finalUrl: string;
|
|
16
|
+
diagnostics: UpstreamUrlDiagnostic[];
|
|
17
|
+
};
|
|
3
18
|
|
|
4
19
|
export function buildUpstreamUrl(upstreamBase: string, normalizedPath: string): string {
|
|
5
20
|
const base = upstreamBase.endsWith("/") ? upstreamBase.slice(0, -1) : upstreamBase;
|
|
6
|
-
const endpointPath = normalizedPath
|
|
21
|
+
const endpointPath = endpointPathFor(normalizedPath);
|
|
7
22
|
const queryIndex = normalizedPath.indexOf("?");
|
|
8
23
|
const search = queryIndex >= 0 ? normalizedPath.slice(queryIndex) : "";
|
|
9
24
|
|
|
@@ -24,6 +39,62 @@ export function buildUpstreamUrl(upstreamBase: string, normalizedPath: string):
|
|
|
24
39
|
return base + normalizedPath;
|
|
25
40
|
}
|
|
26
41
|
|
|
42
|
+
export function previewUpstreamUrl(
|
|
43
|
+
upstreamBase: string,
|
|
44
|
+
normalizedPath: string,
|
|
45
|
+
options: UpstreamUrlPreviewOptions = {},
|
|
46
|
+
): UpstreamUrlPreview {
|
|
47
|
+
const base = upstreamBase.trim().replace(/\/+$/, "");
|
|
48
|
+
const endpointPath = endpointPathFor(normalizedPath);
|
|
49
|
+
const finalUrl = buildUpstreamUrl(base, normalizedPath);
|
|
50
|
+
const diagnostics: UpstreamUrlDiagnostic[] = [];
|
|
51
|
+
const exactEndpoint = endpointPath !== "" && base.endsWith(endpointPath);
|
|
52
|
+
|
|
53
|
+
if (exactEndpoint) {
|
|
54
|
+
diagnostics.push({
|
|
55
|
+
severity: "info",
|
|
56
|
+
message: "This already includes the exact endpoint; no duplicate suffix will be appended.",
|
|
57
|
+
});
|
|
58
|
+
} else if (isZhipuVersionedOpenAIBase(base) && isVersionedChatCompletionsPath(normalizedPath)) {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
severity: "info",
|
|
61
|
+
message: "Zhipu bases already include the API version; only /chat/completions is appended.",
|
|
62
|
+
});
|
|
63
|
+
} else if (base.endsWith("/v1") && normalizedPath.startsWith("/v1/")) {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
severity: "info",
|
|
66
|
+
message: "Base URL already ends with /v1; the suffix will not repeat /v1.",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isResponsesPath(endpointPath) && baseEndsWithChatEndpoint(base)) {
|
|
71
|
+
diagnostics.push({
|
|
72
|
+
severity: "warning",
|
|
73
|
+
message: "This looks like a Chat Completions endpoint, so Responses would append after it.",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isChatCompletionsPath(endpointPath) && baseEndsWithResponsesEndpoint(base)) {
|
|
78
|
+
diagnostics.push({
|
|
79
|
+
severity: "warning",
|
|
80
|
+
message: "This looks like a Responses endpoint, so Chat Completions would append after it.",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isResponsesPath(endpointPath) && isLikelyDeepSeekBase(base, options.providerName)) {
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
severity: "warning",
|
|
87
|
+
message: "DeepSeek's official API does not currently expose OpenAI Responses.",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { finalUrl, diagnostics };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function endpointPathFor(normalizedPath: string): string {
|
|
95
|
+
return normalizedPath.split("?")[0] ?? normalizedPath;
|
|
96
|
+
}
|
|
97
|
+
|
|
27
98
|
function isVersionedChatCompletionsPath(normalizedPath: string): boolean {
|
|
28
99
|
return (
|
|
29
100
|
normalizedPath === PATH_V1_CHAT_COMPLETIONS ||
|
|
@@ -32,6 +103,39 @@ function isVersionedChatCompletionsPath(normalizedPath: string): boolean {
|
|
|
32
103
|
);
|
|
33
104
|
}
|
|
34
105
|
|
|
106
|
+
function isChatCompletionsPath(endpointPath: string): boolean {
|
|
107
|
+
return (
|
|
108
|
+
endpointPath === PATH_V1_CHAT_COMPLETIONS ||
|
|
109
|
+
endpointPath.startsWith(`${PATH_V1_CHAT_COMPLETIONS}/`)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isResponsesPath(endpointPath: string): boolean {
|
|
114
|
+
return endpointPath === PATH_V1_RESPONSES || endpointPath.startsWith(`${PATH_V1_RESPONSES}/`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function baseEndsWithChatEndpoint(base: string): boolean {
|
|
118
|
+
const normalized = base.toLowerCase();
|
|
119
|
+
return normalized.endsWith("/chat/completions");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function baseEndsWithResponsesEndpoint(base: string): boolean {
|
|
123
|
+
const normalized = base.toLowerCase();
|
|
124
|
+
return normalized.endsWith("/responses");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isLikelyDeepSeekBase(base: string, providerName: string | undefined): boolean {
|
|
128
|
+
const provider = providerName?.toLowerCase();
|
|
129
|
+
if (provider !== undefined && provider.includes("deepseek")) return true;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const parsed = new URL(base);
|
|
133
|
+
return parsed.hostname.toLowerCase().includes("deepseek");
|
|
134
|
+
} catch {
|
|
135
|
+
return base.toLowerCase().includes("deepseek");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
35
139
|
function isZhipuVersionedOpenAIBase(base: string): boolean {
|
|
36
140
|
try {
|
|
37
141
|
const parsed = new URL(base);
|