@tonyclaw/agent-inspector 2.1.16 → 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.
Files changed (51) hide show
  1. package/.output/cli.js +57 -13
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-Ccrrcx1j.js → CompareDrawer-B9sLBHw5.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
  5. package/.output/public/assets/{ReplayDialog-C7axhr-l.js → ReplayDialog-D9I9W9n3.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-WOptg8j_.js → RequestAnatomy-DGXK_Rii.js} +1 -1
  7. package/.output/public/assets/{ResponseView-CSAcxh_M.js → ResponseView-df_JzwsS.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-DJS5KhPx.js → StreamingChunkSequence-i3DM5IKL.js} +1 -1
  9. package/.output/public/assets/_sessionId-CTpCHdFh.js +1 -0
  10. package/.output/public/assets/index-Blqvndcn.js +1 -0
  11. package/.output/public/assets/index-Cs79WRQj.css +1 -0
  12. package/.output/public/assets/{index-DMPNh46t.js → index-Dqc2r1ea.js} +1 -1
  13. package/.output/public/assets/{json-viewer-inX5QSa3.js → json-viewer-D3QWQlSB.js} +1 -1
  14. package/.output/public/assets/{main-aCAKYGSD.js → main-ZhxhOCmF.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +220 -202
  16. package/.output/server/{_sessionId-C0fhvQnn.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
  17. package/.output/server/_ssr/{CompareDrawer-CO-Ti5sg.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-N4J7uBCX.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +1858 -1396
  19. package/.output/server/_ssr/{ReplayDialog-J1VxC0In.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
  20. package/.output/server/_ssr/{RequestAnatomy-DBH-F3rq.mjs → RequestAnatomy-HQfrxjO1.mjs} +2 -2
  21. package/.output/server/_ssr/{ResponseView-DLFwV7Hd.mjs → ResponseView-QSlSD11e.mjs} +3 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-BywmuND0.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +2 -2
  23. package/.output/server/_ssr/{index-CStymIc0.mjs → index-D_EzfXaN.mjs} +2 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-DlQu0N1x.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
  26. package/.output/server/_ssr/{router-CQ4Zxhmr.mjs → router-__o2wrfO.mjs} +398 -95
  27. package/.output/server/{_tanstack-start-manifest_v-BHeJBGfo.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
  28. package/.output/server/index.mjs +73 -73
  29. package/README.md +57 -2
  30. package/package.json +1 -1
  31. package/src/cli/startupOutput.ts +16 -0
  32. package/src/cli.ts +61 -15
  33. package/src/components/ProxyViewer.tsx +395 -64
  34. package/src/components/providers/ProviderCard.tsx +132 -31
  35. package/src/components/providers/ProviderForm.tsx +43 -11
  36. package/src/components/providers/ProvidersPanel.tsx +1 -1
  37. package/src/components/proxy-viewer/ConversationHeader.tsx +133 -112
  38. package/src/components/proxy-viewer/LogEntry.tsx +109 -26
  39. package/src/components/proxy-viewer/LogEntryHeader.tsx +339 -329
  40. package/src/components/proxy-viewer/ThreadConnector.tsx +102 -83
  41. package/src/components/proxy-viewer/TurnGroup.tsx +22 -63
  42. package/src/components/proxy-viewer/bodyHydration.ts +56 -0
  43. package/src/lib/providerTestContract.ts +56 -10
  44. package/src/lib/providerTestPrompt.ts +23 -1
  45. package/src/lib/upstreamUrl.ts +105 -1
  46. package/src/mcp/toolHandlers.ts +1 -1
  47. package/src/routes/api/providers.$providerId.test.log.ts +85 -10
  48. package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +0 -106
  49. package/.output/public/assets/_sessionId-DUSalzKH.js +0 -1
  50. package/.output/public/assets/index-BPpA21dY.css +0 -1
  51. package/.output/public/assets/index-Bt0Az2I2.js +0 -1
@@ -1,7 +1,6 @@
1
- import { type JSX, useMemo } from "react";
2
- import { cn } from "../../lib/utils";
1
+ import type { JSX, KeyboardEvent, MouseEvent } from "react";
3
2
  import { isTurnBoundary, type StopReason } from "../../lib/stopReason";
4
- import { getCrabVariant, getInteriorCrabVariant } from "../ui/crab-variants";
3
+ import { cn } from "../../lib/utils";
5
4
 
6
5
  export type ThreadConnectorProps = {
7
6
  stopReason: StopReason;
@@ -11,13 +10,13 @@ export type ThreadConnectorProps = {
11
10
  isTurnStart: boolean;
12
11
  /** True when this entry is the turn's only request. */
13
12
  isOnlyEntry?: boolean;
14
- /** Seed for crab variant selection (0-11). */
15
- crabIndex?: number;
16
- /** When true the crab is clickable (collapse / expand a turn). */
13
+ /** When true the timeline marker is clickable (collapse / expand a turn). */
17
14
  collapsible?: boolean;
18
15
  onToggle?: () => void;
19
16
  };
20
17
 
18
+ type MarkerTone = "start" | "end" | "fused" | "running" | "idle";
19
+
21
20
  function boundaryTitle(stopReason: StopReason): string {
22
21
  switch (stopReason) {
23
22
  case "end_turn":
@@ -32,104 +31,124 @@ function boundaryTitle(stopReason: StopReason): string {
32
31
  }
33
32
  }
34
33
 
35
- /**
36
- * Vertical timeline connector. Top spacer uses a calc() that
37
- * mixes rem (py-1 + half-line) and px (border) so the crab
38
- * centre-line matches the LogEntry display number regardless of the
39
- * root font-size. The bottom spacer is flex-1 so the outgoing
40
- * line adapts to whatever height the LogEntry occupies.
41
- */
34
+ function markerTone({
35
+ isFusedBoundary,
36
+ isBoundary,
37
+ isTurnStart,
38
+ isRunning,
39
+ }: {
40
+ isFusedBoundary: boolean;
41
+ isBoundary: boolean;
42
+ isTurnStart: boolean;
43
+ isRunning: boolean;
44
+ }): MarkerTone {
45
+ if (isFusedBoundary) return "fused";
46
+ if (isBoundary) return "end";
47
+ if (isTurnStart) return "start";
48
+ if (isRunning) return "running";
49
+ return "idle";
50
+ }
51
+
52
+ function markerClass(tone: MarkerTone, interactive: boolean): string {
53
+ const base =
54
+ "inline-flex size-3.5 items-center justify-center rounded-full border transition-colors";
55
+ const cursor = interactive ? "cursor-pointer hover:border-cyan-200/70 hover:bg-cyan-300/20" : "";
56
+
57
+ switch (tone) {
58
+ case "start":
59
+ return cn(base, cursor, "border-cyan-300/50 bg-cyan-300/12 text-cyan-200");
60
+ case "end":
61
+ return cn(base, cursor, "border-amber-300/55 bg-amber-300/12 text-amber-200");
62
+ case "fused":
63
+ return cn(
64
+ base,
65
+ cursor,
66
+ "border-slate-200/50 bg-slate-200/10 text-slate-100 ring-2 ring-cyan-300/15",
67
+ );
68
+ case "running":
69
+ return cn(
70
+ base,
71
+ cursor,
72
+ "border-amber-300/45 bg-amber-300/10 text-amber-200 ring-2 ring-amber-300/10",
73
+ );
74
+ case "idle":
75
+ return cn(base, cursor, "border-muted-foreground/25 bg-muted/30 text-muted-foreground/60");
76
+ }
77
+ }
78
+
79
+ function markerInnerClass(tone: MarkerTone): string {
80
+ switch (tone) {
81
+ case "running":
82
+ return "size-1.5 rounded-full bg-current animate-pulse";
83
+ case "idle":
84
+ return "size-1 rounded-full bg-current opacity-55";
85
+ case "start":
86
+ case "end":
87
+ case "fused":
88
+ return "size-1.5 rounded-full bg-current";
89
+ }
90
+ }
91
+
92
+ function markerTitle(tone: MarkerTone, stopReason: StopReason): string {
93
+ switch (tone) {
94
+ case "start":
95
+ return "Start of turn";
96
+ case "end":
97
+ return boundaryTitle(stopReason);
98
+ case "fused":
99
+ return "Single-request turn";
100
+ case "running":
101
+ return "Processing";
102
+ case "idle":
103
+ return "Request in turn";
104
+ }
105
+ }
106
+
42
107
  export function ThreadConnector({
43
108
  stopReason,
44
109
  isPending,
45
110
  isFirst,
46
111
  isTurnStart,
47
112
  isOnlyEntry = false,
48
- crabIndex = 0,
49
113
  collapsible = false,
50
114
  onToggle,
51
115
  }: ThreadConnectorProps): JSX.Element {
52
116
  const isBoundary = isTurnBoundary(stopReason);
53
117
  const isFusedBoundary = isOnlyEntry && isTurnStart && isBoundary;
54
118
  const isRunning = isPending && !isBoundary;
55
- const Crab = useMemo(() => getCrabVariant(crabIndex), [crabIndex]);
56
- const FusedCrab = useMemo(() => getInteriorCrabVariant(crabIndex), [crabIndex]);
119
+ const tone = markerTone({ isFusedBoundary, isBoundary, isTurnStart, isRunning });
120
+ const interactive = collapsible && onToggle !== undefined;
121
+
122
+ const handleMarkerClick = (event: MouseEvent<HTMLSpanElement>): void => {
123
+ if (!interactive || onToggle === undefined) return;
124
+ event.stopPropagation();
125
+ onToggle();
126
+ };
57
127
 
58
- const interactiveProps =
59
- collapsible && onToggle
60
- ? ({
61
- role: "button" as const,
62
- tabIndex: 0,
63
- className: "cursor-pointer",
64
- onClick: (e: React.MouseEvent) => {
65
- e.stopPropagation();
66
- onToggle();
67
- },
68
- onKeyDown: (e: React.KeyboardEvent) => {
69
- if (e.key === "Enter" || e.key === " ") {
70
- e.preventDefault();
71
- onToggle();
72
- }
73
- },
74
- } as const)
75
- : ({} as const);
128
+ const handleMarkerKeyDown = (event: KeyboardEvent<HTMLSpanElement>): void => {
129
+ if (!interactive || onToggle === undefined) return;
130
+ if (event.key !== "Enter" && event.key !== " ") return;
131
+ event.preventDefault();
132
+ onToggle();
133
+ };
76
134
 
77
135
  return (
78
136
  <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
137
  <div className="flex justify-center h-[calc(0.75rem-8px)]">
84
138
  {!isFirst && <div className="w-0.5 bg-muted-foreground/30 h-full" />}
85
139
  </div>
86
140
 
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
- {isFusedBoundary ? (
91
- <span title="Start and end of turn">
92
- <FusedCrab
93
- className={cn(
94
- "size-3.5 text-cyan-300/90",
95
- "animate-crab-settle",
96
- "drop-shadow-[0_0_4px_rgba(103,232,249,0.35)]",
97
- )}
98
- />
99
- </span>
100
- ) : isBoundary ? (
101
- <span title={boundaryTitle(stopReason)} {...interactiveProps}>
102
- <Crab
103
- className={cn(
104
- "size-3.5 text-amber-400",
105
- "animate-crab-settle",
106
- "drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]",
107
- )}
108
- />
109
- </span>
110
- ) : isTurnStart ? (
111
- <span title="Start of turn" {...interactiveProps}>
112
- <Crab
113
- className={cn(
114
- "size-3.5 text-cyan-300/85",
115
- "animate-crab-appear",
116
- "drop-shadow-[0_0_4px_rgba(103,232,249,0.32)]",
117
- )}
118
- />
119
- </span>
120
- ) : isRunning ? (
121
- <span title="Processing…">
122
- <Crab className={cn("size-3.5 text-amber-300/80", "animate-crab-crawl")} />
123
- </span>
124
- ) : (
125
- <span>
126
- <Crab className="size-3.5 text-muted-foreground/40" />
127
- </span>
128
- )}
141
+ <span
142
+ role={interactive ? "button" : undefined}
143
+ tabIndex={interactive ? 0 : undefined}
144
+ title={markerTitle(tone, stopReason)}
145
+ className={markerClass(tone, interactive)}
146
+ onClick={handleMarkerClick}
147
+ onKeyDown={handleMarkerKeyDown}
148
+ >
149
+ <span className={markerInnerClass(tone)} />
150
+ </span>
129
151
 
130
- {/* Bottom spacer — flex-1 fills whatever height the LogEntry
131
- * consumes (header + expanded content). Turn boundaries have
132
- * no outgoing line. */}
133
152
  <div className="flex-1 flex justify-center min-h-0">
134
153
  {!isBoundary && (
135
154
  <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
- {/* Dual-crab connector for collapsed multi-request turns. */}
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-0.5 bg-muted-foreground/30 h-full" />
231
+ <div className="w-px bg-muted-foreground/25 h-full" />
263
232
  </div>
264
-
265
233
  <span
266
- role="button"
267
- tabIndex={0}
268
- title="End of Turn - click to expand"
269
- className="cursor-pointer"
270
- onClick={(e) => {
271
- e.stopPropagation();
272
- toggleCollapse();
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-lg border border-cyan-300/10 py-1.5 px-3 flex items-center gap-3 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
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={ROW_CHEVRON_SLOT_CLASS}>
300
- <ChevronRight className="size-4 shrink-0" />
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-400/80 font-mono font-semibold tabular-nums shrink-0"
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>
@@ -389,7 +349,6 @@ export const TurnGroup = memo(function TurnGroup({
389
349
  isFirst={visibleIdx === 0}
390
350
  isTurnStart={isTurnStart}
391
351
  isOnlyEntry={isSingleLog}
392
- crabIndex={log.id % 12}
393
352
  collapsible={collapsible && isTurnStart}
394
353
  onToggle={toggleCollapse}
395
354
  />
@@ -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
- const ModelTestResultsSchema = z.object({
76
- anthropic: ProviderFormatTestResultsSchema,
77
- openai: ProviderFormatTestResultsSchema,
78
- });
75
+ export type ProviderFormatTestResults = z.infer<typeof ProviderFormatTestResultsSchema>;
79
76
 
80
- export const ProviderTestResultsSchema = z.object({
81
- anthropic: ProviderFormatTestResultsSchema,
82
- openai: ProviderFormatTestResultsSchema,
83
- models: z.record(z.string(), ModelTestResultsSchema).optional(),
84
- testedAt: z.string().optional(),
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 ProviderTestRequestBody = {
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),