@tonyclaw/agent-inspector 2.1.14 → 2.1.16

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 (70) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-Ccrrcx1j.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +106 -0
  4. package/.output/public/assets/ReplayDialog-C7axhr-l.js +1 -0
  5. package/.output/public/assets/{RequestAnatomy-BNahe83D.js → RequestAnatomy-WOptg8j_.js} +1 -1
  6. package/.output/public/assets/{ResponseView-DSOnGqi6.js → ResponseView-CSAcxh_M.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-BEKTDklB.js → StreamingChunkSequence-DJS5KhPx.js} +1 -1
  8. package/.output/public/assets/_sessionId-DUSalzKH.js +1 -0
  9. package/.output/public/assets/index-BPpA21dY.css +1 -0
  10. package/.output/public/assets/index-Bt0Az2I2.js +1 -0
  11. package/.output/public/assets/{index-DWOkqdCa.js → index-DMPNh46t.js} +1 -1
  12. package/.output/public/assets/{json-viewer-C2JpgcW0.js → json-viewer-inX5QSa3.js} +1 -1
  13. package/.output/public/assets/{main-CSONBwwn.js → main-aCAKYGSD.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +224 -194
  15. package/.output/server/{_sessionId-CPAa37n5.mjs → _sessionId-C0fhvQnn.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-ceW5VxMo.mjs → CompareDrawer-CO-Ti5sg.mjs} +36 -15
  17. package/.output/server/_ssr/{ProxyViewerContainer-CDfEE_w-.mjs → ProxyViewerContainer-N4J7uBCX.mjs} +1153 -494
  18. package/.output/server/_ssr/{ReplayDialog-V0s_eEbR.mjs → ReplayDialog-J1VxC0In.mjs} +14 -7
  19. package/.output/server/_ssr/{RequestAnatomy-f1ccwR9d.mjs → RequestAnatomy-DBH-F3rq.mjs} +3 -3
  20. package/.output/server/_ssr/{ResponseView-BIRrqG4H.mjs → ResponseView-DLFwV7Hd.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-V3JFjCgX.mjs → StreamingChunkSequence-BywmuND0.mjs} +3 -3
  22. package/.output/server/_ssr/{index-DsykulzS.mjs → index-CStymIc0.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-Dcnm0Ivf.mjs → json-viewer-DlQu0N1x.mjs} +3 -3
  25. package/.output/server/_ssr/{router-4bdm6Mt2.mjs → router-CQ4Zxhmr.mjs} +118 -47
  26. package/.output/server/{_tanstack-start-manifest_v-Xp4CO64V.mjs → _tanstack-start-manifest_v-BHeJBGfo.mjs} +1 -1
  27. package/.output/server/index.mjs +75 -75
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +174 -107
  30. package/src/components/ProxyViewerContainer.tsx +77 -67
  31. package/src/components/alerts/AlertsDialog.tsx +2 -1
  32. package/src/components/clients/ClientLogo.tsx +132 -0
  33. package/src/components/groups/GroupsDialog.tsx +37 -14
  34. package/src/components/providers/ProviderCard.tsx +1 -1
  35. package/src/components/providers/ProviderForm.tsx +18 -18
  36. package/src/components/providers/ProviderLogo.tsx +6 -1
  37. package/src/components/providers/ProvidersPanel.tsx +6 -6
  38. package/src/components/providers/SettingsDialog.tsx +13 -6
  39. package/src/components/proxy-viewer/AgentTraceSummary.tsx +62 -29
  40. package/src/components/proxy-viewer/CompareDrawer.tsx +38 -8
  41. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  42. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  43. package/src/components/proxy-viewer/ConversationHeader.tsx +65 -61
  44. package/src/components/proxy-viewer/LogEntry.tsx +31 -18
  45. package/src/components/proxy-viewer/LogEntryHeader.tsx +42 -49
  46. package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
  47. package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
  48. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  49. package/src/components/proxy-viewer/ThreadConnector.tsx +22 -11
  50. package/src/components/proxy-viewer/TurnGroup.tsx +30 -36
  51. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  52. package/src/components/proxy-viewer/viewerState.ts +5 -6
  53. package/src/components/ui/icon-trigger.ts +2 -0
  54. package/src/components/ui/transient-toast.tsx +104 -0
  55. package/src/lib/sessionInfoContract.ts +5 -0
  56. package/src/lib/stopReason.ts +48 -16
  57. package/src/mcp/server.ts +71 -27
  58. package/src/mcp/toolHandlers.ts +23 -4
  59. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  60. package/src/proxy/sessionInfo.ts +36 -3
  61. package/src/proxy/sessionSupervisor.ts +20 -1
  62. package/src/proxy/store.ts +1 -1
  63. package/src/routes/api/providers.$providerId.test.log.ts +8 -4
  64. package/styles/globals.css +180 -180
  65. package/.output/public/assets/CompareDrawer-DjgjIFx7.js +0 -1
  66. package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +0 -106
  67. package/.output/public/assets/ReplayDialog-CU0Tbb2c.js +0 -1
  68. package/.output/public/assets/_sessionId-Cif8JZdn.js +0 -1
  69. package/.output/public/assets/index-D_WfwzUi.js +0 -1
  70. package/.output/public/assets/index-DtLuQrs0.css +0 -1
@@ -14,8 +14,8 @@ import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
14
14
  import { cn, formatTokens } from "../../lib/utils";
15
15
  import type { CapturedLog } from "../../contracts";
16
16
  import { getCrabVariant } from "../ui/crab-variants";
17
- import { ProviderLogo, detectProvider, type Provider } from "../providers/ProviderLogo";
18
17
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
18
+ import { ROW_CHEVRON_SLOT_CLASS } from "./ProviderLogoStack";
19
19
  import type { CacheTrendEntry } from "./cacheTrend";
20
20
  import { LogEntry } from "./LogEntry";
21
21
  import {
@@ -48,6 +48,8 @@ export type TurnGroupProps = {
48
48
  turnIndex?: number;
49
49
  timeDisplayFormat: TimeDisplayFormat;
50
50
  pendingFocusRequest?: PendingTurnFocusRequest;
51
+ /** Show client PID/project metadata on each log row. */
52
+ showLogClientMetadata?: boolean;
51
53
  };
52
54
 
53
55
  export const TurnGroup = memo(function TurnGroup({
@@ -61,6 +63,7 @@ export const TurnGroup = memo(function TurnGroup({
61
63
  turnIndex = 0,
62
64
  timeDisplayFormat,
63
65
  pendingFocusRequest,
66
+ showLogClientMetadata = true,
64
67
  }: TurnGroupProps): JSX.Element {
65
68
  const lastIdx = entries.length - 1;
66
69
  const lastStop = entries[lastIdx]?.stopReason ?? null;
@@ -70,7 +73,7 @@ export const TurnGroup = memo(function TurnGroup({
70
73
  const collapsible = isTurnCollapsible(entries.length, isComplete, isPending);
71
74
  const [collapsed, setCollapsed] = useState(false);
72
75
 
73
- // Auto-collapse when the turn finishes (transitions from incomplete complete)
76
+ // Auto-collapse when the turn finishes (transitions from incomplete to complete)
74
77
  const prevCompleteRef = useRef(false);
75
78
  useEffect(() => {
76
79
  if (!collapsible) {
@@ -142,21 +145,11 @@ export const TurnGroup = memo(function TurnGroup({
142
145
  };
143
146
  }, [entries, lastIdx]);
144
147
 
145
- // Unique providers across all entries (for collapsed model logos)
146
- const uniqueProviders = useMemo(() => {
147
- const seen = new Set<Provider>();
148
- for (const e of entries) {
149
- const p = detectProvider(e.log.model);
150
- if (p !== "unknown") seen.add(p);
151
- }
152
- return [...seen];
153
- }, [entries]);
154
-
155
148
  // Crab variant creators for the dual-crab collapsed layout
156
149
  const StartCrab = useMemo(() => getCrabVariant(entries[0]?.log.id ?? 0), [entries]);
157
150
  const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
158
151
 
159
- const bgClass = turnIndex % 2 === 0 ? "bg-[#070a0f]" : "bg-[#090d12]";
152
+ const bgClass = turnIndex % 2 === 0 ? "bg-white/[0.012]" : "bg-cyan-300/[0.012]";
160
153
  const aggregateIsSlow =
161
154
  aggregate.maxElapsed !== null &&
162
155
  slowResponseThresholdSeconds > 0 &&
@@ -170,7 +163,7 @@ export const TurnGroup = memo(function TurnGroup({
170
163
  return events;
171
164
  }, [entries]);
172
165
 
173
- // ResizeObserver re-render connectors when any LogEntry height changes
166
+ // ResizeObserver re-renders connectors when any LogEntry height changes
174
167
  const [layoutVersion, setLayoutVersion] = useState(0);
175
168
  const containerRef = useRef<HTMLDivElement>(null);
176
169
  useEffect(() => {
@@ -207,7 +200,7 @@ export const TurnGroup = memo(function TurnGroup({
207
200
  aria-label={collapsed ? undefined : turnLabel}
208
201
  data-nav-id={collapsed ? undefined : `turn-${String(firstLogId)}`}
209
202
  className={cn(
210
- "border rounded-lg transition-[border-color,box-shadow]",
203
+ "rounded-[8px] border transition-[border-color,box-shadow]",
211
204
  isPending
212
205
  ? "border-amber-300/15 shadow-[0_0_0_1px_rgba(252,211,77,0.04)]"
213
206
  : "border-transparent",
@@ -244,7 +237,7 @@ export const TurnGroup = memo(function TurnGroup({
244
237
  <span
245
238
  role="button"
246
239
  tabIndex={0}
247
- title="Start of turn click to expand"
240
+ title="Start of turn - click to expand"
248
241
  className="cursor-pointer"
249
242
  onClick={(e) => {
250
243
  e.stopPropagation();
@@ -259,8 +252,8 @@ export const TurnGroup = memo(function TurnGroup({
259
252
  >
260
253
  <StartCrab
261
254
  className={cn(
262
- "size-3.5 text-emerald-400",
263
- "animate-crab-appear drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
255
+ "size-3.5 text-cyan-300/85",
256
+ "animate-crab-appear drop-shadow-[0_0_4px_rgba(103,232,249,0.32)]",
264
257
  )}
265
258
  />
266
259
  </span>
@@ -272,7 +265,7 @@ export const TurnGroup = memo(function TurnGroup({
272
265
  <span
273
266
  role="button"
274
267
  tabIndex={0}
275
- title="End of Turn click to expand"
268
+ title="End of Turn - click to expand"
276
269
  className="cursor-pointer"
277
270
  onClick={(e) => {
278
271
  e.stopPropagation();
@@ -294,17 +287,28 @@ export const TurnGroup = memo(function TurnGroup({
294
287
  </span>
295
288
  </div>
296
289
 
297
- {/* Summary content hidden for single-log turns. */}
290
+ {/* Summary content - hidden for single-log turns. */}
298
291
  {entries.length > 1 && (
299
292
  <div
300
293
  className={cn(
301
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",
302
296
  bgClass,
303
297
  )}
304
298
  >
299
+ <span className={ROW_CHEVRON_SLOT_CLASS}>
300
+ <ChevronRight className="size-4 shrink-0" />
301
+ </span>
302
+
305
303
  {/* ID range */}
306
- <span className="text-blue-400/80 font-mono font-semibold tabular-nums shrink-0">
307
- #{entries[0]?.log.id ?? "?"} ~ #{entries[lastIdx]?.log.id ?? "?"}
304
+ <span
305
+ className="text-blue-400/80 font-mono font-semibold tabular-nums shrink-0"
306
+ title={`Log IDs ${String(entries[0]?.log.id ?? "?")} ~ ${String(
307
+ entries[lastIdx]?.log.id ?? "?",
308
+ )}`}
309
+ >
310
+ #{entries[0]?.sessionLogNumber ?? "?"} ~ #
311
+ {entries[lastIdx]?.sessionLogNumber ?? "?"}
308
312
  </span>
309
313
 
310
314
  {/* Request count */}
@@ -312,16 +316,7 @@ export const TurnGroup = memo(function TurnGroup({
312
316
  {entries.length} request{entries.length > 1 ? "s" : ""}
313
317
  </span>
314
318
 
315
- {/* Model logos one per unique provider */}
316
- {uniqueProviders.length > 0 && (
317
- <span className="flex items-center gap-0.5 shrink-0">
318
- {uniqueProviders.map((p) => (
319
- <ProviderLogo key={p} provider={p} className="size-4" />
320
- ))}
321
- </span>
322
- )}
323
-
324
- {/* Elapsed — slowest single request in the turn (not the sum) */}
319
+ {/* Elapsed - slowest single request in the turn (not the sum) */}
325
320
  {aggregate.maxElapsed !== null && (
326
321
  <TooltipProvider>
327
322
  <Tooltip>
@@ -368,9 +363,6 @@ export const TurnGroup = memo(function TurnGroup({
368
363
 
369
364
  {/* Spacer */}
370
365
  <span className="flex-1 min-w-0" />
371
-
372
- {/* Expand chevron */}
373
- <ChevronRight className="size-4 text-muted-foreground shrink-0" />
374
366
  </div>
375
367
  )}
376
368
  </div>
@@ -401,14 +393,16 @@ export const TurnGroup = memo(function TurnGroup({
401
393
  collapsible={collapsible && isTurnStart}
402
394
  onToggle={toggleCollapse}
403
395
  />
404
- <div className={cn("flex-1 min-w-0 mb-0.5 rounded-lg", bgClass)}>
396
+ <div className={cn("flex-1 min-w-0 mb-0.5 rounded-[8px]", bgClass)}>
405
397
  <LogEntry
406
398
  log={log}
399
+ displayNumber={entry.sessionLogNumber}
407
400
  viewMode={viewMode}
408
401
  strip={strip}
409
402
  slowResponseThresholdSeconds={slowResponseThresholdSeconds}
410
403
  timeDisplayFormat={timeDisplayFormat}
411
404
  cacheTrend={cacheTrends?.get(log.id) ?? null}
405
+ showClientMetadata={showLogClientMetadata}
412
406
  onCompareWithPrevious={
413
407
  comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
414
408
  }
@@ -21,6 +21,8 @@ type TurnGroupListProps = {
21
21
  onCompareWithPrevious: (log: CapturedLog) => void;
22
22
  comparisonPredecessors: Map<number, CapturedLog>;
23
23
  timeDisplayFormat: TimeDisplayFormat;
24
+ /** Show client PID/project metadata on each log row. */
25
+ showLogClientMetadata?: boolean;
24
26
  };
25
27
 
26
28
  type PendingVirtualFocusRequest = PendingTurnFocusRequest & {
@@ -73,6 +75,7 @@ function renderTurnGroup(
73
75
  comparisonPredecessors={props.comparisonPredecessors}
74
76
  turnIndex={turnGroup.turnIndex}
75
77
  pendingFocusRequest={pendingFocusRequest}
78
+ showLogClientMetadata={props.showLogClientMetadata}
76
79
  />
77
80
  );
78
81
  }
@@ -6,6 +6,7 @@ import { resolveLogFormat } from "./log-formats";
6
6
  export type TurnEntry = {
7
7
  log: CapturedLog;
8
8
  stopReason: StopReason;
9
+ sessionLogNumber: number;
9
10
  };
10
11
 
11
12
  export type TurnGroupData = {
@@ -68,10 +69,6 @@ function responseMayContainToolTrace(log: CapturedLog, format: ResolvedLogFormat
68
69
  }
69
70
  }
70
71
 
71
- export function shouldRenderConversationContent(standalone: boolean, expanded: boolean): boolean {
72
- return standalone || expanded;
73
- }
74
-
75
72
  export function isTurnCollapsible(
76
73
  entryCount: number,
77
74
  isComplete: boolean,
@@ -85,8 +82,10 @@ export function buildTurnGroups(logs: CapturedLog[]): TurnGroupData[] {
85
82
  let entries: TurnEntry[] = [];
86
83
  let turnIndex = 0;
87
84
 
88
- for (const log of logs) {
89
- entries.push({ log, stopReason: extractStopReason(log) });
85
+ for (let index = 0; index < logs.length; index += 1) {
86
+ const log = logs[index];
87
+ if (log === undefined) continue;
88
+ entries.push({ log, stopReason: extractStopReason(log), sessionLogNumber: index + 1 });
90
89
  const current = entries[entries.length - 1];
91
90
  if (current !== undefined && isTurnBoundary(current.stopReason)) {
92
91
  groups.push({ entries, turnIndex });
@@ -0,0 +1,2 @@
1
+ export const INSPECTOR_ICON_TRIGGER_CLASS =
2
+ "relative size-8 rounded-md border border-input bg-background text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50";
@@ -0,0 +1,104 @@
1
+ import { AlertTriangle, CheckCircle2, X } from "lucide-react";
2
+ import { useCallback, useEffect, useState, type JSX } from "react";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ export const TRANSIENT_TOAST_AUTO_DISMISS_MS = 3000;
7
+
8
+ export type TransientToastTone = "error" | "success";
9
+
10
+ export type TransientToastInput = {
11
+ message: string;
12
+ tone?: TransientToastTone;
13
+ };
14
+
15
+ export type TransientToastNotice = {
16
+ id: number;
17
+ message: string;
18
+ tone: TransientToastTone;
19
+ };
20
+
21
+ export function createNextTransientToast(
22
+ previous: TransientToastNotice | null,
23
+ input: TransientToastInput,
24
+ ): TransientToastNotice {
25
+ return {
26
+ id: (previous?.id ?? 0) + 1,
27
+ message: input.message,
28
+ tone: input.tone ?? "error",
29
+ };
30
+ }
31
+
32
+ export function useTransientToast(): {
33
+ notice: TransientToastNotice | null;
34
+ showToast: (input: TransientToastInput) => void;
35
+ dismissToast: () => void;
36
+ } {
37
+ const [notice, setNotice] = useState<TransientToastNotice | null>(null);
38
+
39
+ const showToast = useCallback((input: TransientToastInput) => {
40
+ setNotice((previous) => createNextTransientToast(previous, input));
41
+ }, []);
42
+
43
+ const dismissToast = useCallback(() => {
44
+ setNotice(null);
45
+ }, []);
46
+
47
+ useEffect(() => {
48
+ if (notice === null) return undefined;
49
+ const timeout = window.setTimeout(() => {
50
+ setNotice(null);
51
+ }, TRANSIENT_TOAST_AUTO_DISMISS_MS);
52
+
53
+ return () => {
54
+ window.clearTimeout(timeout);
55
+ };
56
+ }, [notice]);
57
+
58
+ return { notice, showToast, dismissToast };
59
+ }
60
+
61
+ export function TransientToast({
62
+ notice,
63
+ onDismiss,
64
+ }: {
65
+ notice: TransientToastNotice | null;
66
+ onDismiss: () => void;
67
+ }): JSX.Element | null {
68
+ if (notice === null) return null;
69
+
70
+ const isError = notice.tone === "error";
71
+ const Icon = isError ? AlertTriangle : CheckCircle2;
72
+
73
+ return (
74
+ <div
75
+ aria-atomic="true"
76
+ aria-live="polite"
77
+ className="pointer-events-none fixed top-4 right-4 z-[100] w-[min(calc(100vw-2rem),26rem)]"
78
+ role="status"
79
+ >
80
+ <div
81
+ key={notice.id}
82
+ className={cn(
83
+ "pointer-events-auto flex items-start gap-2 rounded-md border px-3 py-2 text-sm shadow-[0_18px_50px_rgba(0,0,0,0.45)] backdrop-blur",
84
+ isError
85
+ ? "border-red-400/30 bg-[#16090b]/95 text-red-100"
86
+ : "border-cyan-300/25 bg-[#071318]/95 text-cyan-100",
87
+ )}
88
+ >
89
+ <Icon
90
+ className={cn("mt-0.5 size-4 shrink-0", isError ? "text-red-300" : "text-cyan-200")}
91
+ />
92
+ <div className="min-w-0 flex-1 break-words leading-5">{notice.message}</div>
93
+ <button
94
+ type="button"
95
+ className="inline-flex size-6 shrink-0 items-center justify-center rounded-md text-current/70 transition-colors hover:bg-white/10 hover:text-current"
96
+ aria-label="Dismiss notification"
97
+ onClick={onDismiss}
98
+ >
99
+ <X className="size-3.5" />
100
+ </button>
101
+ </div>
102
+ </div>
103
+ );
104
+ }
@@ -10,6 +10,7 @@ export const SessionTokenUsageSchema = z.object({
10
10
 
11
11
  export const SessionLogSummarySchema = z.object({
12
12
  id: z.number().int().positive(),
13
+ sessionLogNumber: z.number().int().positive().optional(),
13
14
  timestamp: z.string(),
14
15
  provider: z.string().nullable(),
15
16
  model: z.string().nullable(),
@@ -31,6 +32,9 @@ export const SessionLogSummarySchema = z.object({
31
32
  cacheRead: z.number().nullable(),
32
33
  }),
33
34
  sessionId: z.string().nullable(),
35
+ userAgent: z.string().nullable().optional(),
36
+ clientPid: z.number().nullable().optional(),
37
+ clientProjectFolder: z.string().nullable().optional(),
34
38
  });
35
39
 
36
40
  export const SessionInfoSchema = z.object({
@@ -55,6 +59,7 @@ export const SessionInfoSchema = z.object({
55
59
  tokenUsage: SessionTokenUsageSchema,
56
60
  lastLogId: z.number().nullable(),
57
61
  lastModel: z.string().nullable(),
62
+ clientUserAgent: z.string().nullable().optional(),
58
63
  clientPid: z.number().nullable(),
59
64
  clientProjectFolder: z.string().nullable(),
60
65
  inspectorPath: z.string(),
@@ -1,11 +1,41 @@
1
1
  import type { CapturedLog } from "../contracts";
2
2
 
3
- export type StopReason = "end_turn" | "tool_use" | "stop" | null;
3
+ export type StopReason = "end_turn" | "tool_use" | "stop" | "length" | null;
4
4
 
5
5
  function isRecord(value: unknown): value is Record<string, unknown> {
6
6
  return typeof value === "object" && value !== null && !Array.isArray(value);
7
7
  }
8
8
 
9
+ function normalizeAnthropicStopReason(value: string): StopReason {
10
+ switch (value) {
11
+ case "end_turn":
12
+ case "tool_use":
13
+ return value;
14
+ default:
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function normalizeOpenAIFinishReason(value: string): StopReason {
20
+ switch (value) {
21
+ case "stop":
22
+ case "length":
23
+ return value;
24
+ default:
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function isResponsesLengthLimitReason(value: string): boolean {
30
+ switch (value) {
31
+ case "max_output_tokens":
32
+ case "max_tokens":
33
+ return true;
34
+ default:
35
+ return false;
36
+ }
37
+ }
38
+
9
39
  /**
10
40
  * Extracts the stop/finish reason from a captured log's response text.
11
41
  * Detects the response body shape (Anthropic or OpenAI) independently of
@@ -26,21 +56,16 @@ export function extractStopReason(log: CapturedLog): StopReason {
26
56
 
27
57
  // Anthropic shape: { stop_reason: "end_turn" | "tool_use" | ... }
28
58
  if (typeof json.stop_reason === "string") {
29
- if (json.stop_reason === "end_turn" || json.stop_reason === "tool_use") {
30
- return json.stop_reason;
31
- }
32
- return null;
59
+ return normalizeAnthropicStopReason(json.stop_reason);
33
60
  }
34
61
 
35
- // OpenAI shape: { choices: [{ finish_reason: "stop" | ... }] }
36
- if (
37
- Array.isArray(json.choices) &&
38
- json.choices.length > 0 &&
39
- isRecord(json.choices[0]) &&
40
- typeof json.choices[0].finish_reason === "string" &&
41
- json.choices[0].finish_reason === "stop"
42
- ) {
43
- return "stop";
62
+ // OpenAI Chat shape: { choices: [{ finish_reason: "stop" | "length" | ... }] }
63
+ if (Array.isArray(json.choices)) {
64
+ for (const choice of json.choices) {
65
+ if (!isRecord(choice) || typeof choice.finish_reason !== "string") continue;
66
+ const reason = normalizeOpenAIFinishReason(choice.finish_reason);
67
+ if (reason !== null) return reason;
68
+ }
44
69
  }
45
70
 
46
71
  // OpenAI Responses shape: { status: "completed", output: [...] }
@@ -55,6 +80,13 @@ export function extractStopReason(log: CapturedLog): StopReason {
55
80
  }
56
81
  }
57
82
 
83
+ if (json.status === "incomplete" && isRecord(json.incomplete_details)) {
84
+ const reason = json.incomplete_details.reason;
85
+ if (typeof reason === "string" && isResponsesLengthLimitReason(reason)) {
86
+ return "length";
87
+ }
88
+ }
89
+
58
90
  return null;
59
91
  } catch {
60
92
  return null;
@@ -63,8 +95,8 @@ export function extractStopReason(log: CapturedLog): StopReason {
63
95
 
64
96
  /**
65
97
  * Returns true when the stop reason indicates the assistant completed its
66
- * turn naturally (Anthropic end_turn or OpenAI stop).
98
+ * turn, including responses truncated by output length limits.
67
99
  */
68
100
  export function isTurnBoundary(stopReason: StopReason): boolean {
69
- return stopReason === "end_turn" || stopReason === "stop";
101
+ return stopReason === "end_turn" || stopReason === "stop" || stopReason === "length";
70
102
  }
package/src/mcp/server.ts CHANGED
@@ -185,7 +185,7 @@ Parameters:
185
185
  Returns: array of "thick summary" objects, each containing:
186
186
  - id, timestamp, provider, model, apiFormat, method, path, status, isStreaming,
187
187
  hasError, latencyMs, tokens { input, output, cacheCreate, cacheRead },
188
- sessionId, clientPid,
188
+ sessionId, userAgent, clientPid, clientCwd, clientProjectFolder,
189
189
  - lastUserMessagePreview (string|null, ≤500 chars) — first text block of the last user message, parsed via the format-specific schema (NOT a raw body slice)
190
190
  - responsePreview (string|null, ≤500 chars) — first text block of the assistant response
191
191
  - hasChunks, hasRawRequestBody (booleans)
@@ -195,6 +195,72 @@ Either preview field is null when the body is unknown format, unparseable, or th
195
195
  const PROVIDER_WRITE_WARNING =
196
196
  "⚠ This tool mutates provider configuration. Confirm with the user before invoking.";
197
197
 
198
+ const AddProviderInputSchema = z
199
+ .object({
200
+ name: z.string().min(1).describe("Provider display name."),
201
+ apiKey: z.string().min(1).describe("Plaintext provider API key."),
202
+ format: z
203
+ .enum(["anthropic", "openai"])
204
+ .optional()
205
+ .describe("Primary API family. Use openai for Chat Completions or Responses providers."),
206
+ model: z
207
+ .string()
208
+ .min(1)
209
+ .optional()
210
+ .describe("Legacy single-model input. Prefer models for new clients."),
211
+ models: z.array(z.string().min(1)).min(1).optional().describe("Provider model names."),
212
+ anthropicBaseUrl: z
213
+ .string()
214
+ .optional()
215
+ .describe("Anthropic-compatible base or exact /v1/messages endpoint."),
216
+ openaiBaseUrl: z
217
+ .string()
218
+ .optional()
219
+ .describe("OpenAI Chat Completions base or exact /v1/chat/completions endpoint."),
220
+ openaiResponsesBaseUrl: z
221
+ .string()
222
+ .optional()
223
+ .describe(
224
+ "OpenAI Responses base or exact /v1/responses endpoint. MiniMax default endpoint is https://api.minimaxi.com/v1/responses.",
225
+ ),
226
+ authHeader: z.enum(["bearer", "x-api-key"]).optional(),
227
+ apiDocsUrl: z.string().optional(),
228
+ })
229
+ .refine(
230
+ (input) => input.model !== undefined || (input.models !== undefined && input.models.length > 0),
231
+ { message: "Provide models or legacy model." },
232
+ );
233
+
234
+ const UpdateProviderInputSchema = z.object({
235
+ id: z.string().describe("The provider id to update."),
236
+ name: z.string().min(1).optional(),
237
+ apiKey: z.string().min(1).optional(),
238
+ format: z.enum(["anthropic", "openai"]).optional(),
239
+ model: z.string().min(1).optional().describe("Legacy single-model input. Prefer models."),
240
+ models: z.array(z.string().min(1)).optional().describe("Replace provider model names."),
241
+ baseUrl: z
242
+ .string()
243
+ .min(1)
244
+ .optional()
245
+ .describe("Legacy route URL retained for old configs; prefer format-specific URL fields."),
246
+ authHeader: z.enum(["bearer", "x-api-key"]).optional(),
247
+ anthropicBaseUrl: z
248
+ .string()
249
+ .optional()
250
+ .describe("Anthropic-compatible base or exact /v1/messages endpoint."),
251
+ openaiBaseUrl: z
252
+ .string()
253
+ .optional()
254
+ .describe("OpenAI Chat Completions base or exact /v1/chat/completions endpoint."),
255
+ openaiResponsesBaseUrl: z
256
+ .string()
257
+ .optional()
258
+ .describe(
259
+ "OpenAI Responses base or exact /v1/responses endpoint. MiniMax default endpoint is https://api.minimaxi.com/v1/responses.",
260
+ ),
261
+ apiDocsUrl: z.string().optional(),
262
+ });
263
+
198
264
  const LogResourceListResponseSchema = z.object({
199
265
  logs: z.array(z.object({ id: z.number().int().positive() })),
200
266
  });
@@ -1049,18 +1115,8 @@ function registerTools(server: McpServer): void {
1049
1115
  "inspector_add_provider",
1050
1116
  {
1051
1117
  title: "Add a new LLM provider",
1052
- description: `${PROVIDER_WRITE_WARNING}\n\nPersists a new provider to <dataDir>/providers.json. Required fields: name, apiKey, format (anthropic|openai), model. Optional: baseUrl, authHeader (default: bearer), apiDocsUrl.`,
1053
- inputSchema: z.object({
1054
- name: z.string().min(1),
1055
- apiKey: z.string().min(1),
1056
- format: z.enum(["anthropic", "openai"]),
1057
- model: z.string().min(1),
1058
- anthropicBaseUrl: z.string().optional(),
1059
- openaiBaseUrl: z.string().optional(),
1060
- openaiResponsesBaseUrl: z.string().optional(),
1061
- authHeader: z.enum(["bearer", "x-api-key"]).optional(),
1062
- apiDocsUrl: z.string().optional(),
1063
- }),
1118
+ description: `${PROVIDER_WRITE_WARNING}\n\nPersists a new provider to <dataDir>/providers.json. Required fields: name, apiKey, and models (or legacy model). Chat Completions and Responses can use different upstream URLs: set openaiBaseUrl for /v1/chat/completions and openaiResponsesBaseUrl for /v1/responses. The URL builder accepts either a base URL such as https://api.minimaxi.com or an exact endpoint such as https://api.minimaxi.com/v1/responses.`,
1119
+ inputSchema: AddProviderInputSchema,
1064
1120
  },
1065
1121
  (provider) => safeWriteCall("inspector_add_provider", () => addProviderImpl(callApi, provider)),
1066
1122
  );
@@ -1071,20 +1127,8 @@ function registerTools(server: McpServer): void {
1071
1127
  title: "Update an existing provider",
1072
1128
  description: `${PROVIDER_WRITE_WARNING} Updating with nonsense values effectively soft-deletes a provider. Confirm with the user before invoking.
1073
1129
 
1074
- PATCH-style update: only the fields you supply are changed. Returns the updated ProviderConfig.`,
1075
- inputSchema: z.object({
1076
- id: z.string().describe("The provider id to update."),
1077
- name: z.string().min(1).optional(),
1078
- apiKey: z.string().min(1).optional(),
1079
- format: z.enum(["anthropic", "openai"]).optional(),
1080
- model: z.string().min(1).optional(),
1081
- baseUrl: z.string().min(1).optional(),
1082
- authHeader: z.enum(["bearer", "x-api-key"]).optional(),
1083
- anthropicBaseUrl: z.string().optional(),
1084
- openaiBaseUrl: z.string().optional(),
1085
- openaiResponsesBaseUrl: z.string().optional(),
1086
- apiDocsUrl: z.string().optional(),
1087
- }),
1130
+ PATCH-style update: only the fields you supply are changed. Use models to update the configured model list. For OpenAI-compatible providers, openaiBaseUrl targets /v1/chat/completions and openaiResponsesBaseUrl targets /v1/responses, and those endpoints may be different.`,
1131
+ inputSchema: UpdateProviderInputSchema,
1088
1132
  },
1089
1133
  (input) => safeWriteCall("inspector_update_provider", () => updateProviderImpl(callApi, input)),
1090
1134
  );
@@ -175,7 +175,10 @@ export function buildLogSummary(log: CapturedLog) {
175
175
  cacheRead: log.cacheReadInputTokens,
176
176
  },
177
177
  sessionId: log.sessionId,
178
+ userAgent: log.userAgent ?? null,
178
179
  clientPid: log.clientPid ?? null,
180
+ clientCwd: log.clientCwd ?? null,
181
+ clientProjectFolder: log.clientProjectFolder ?? null,
179
182
  lastUserMessagePreview: extractLastUserMessagePreview(log),
180
183
  responsePreview: extractResponsePreview(log),
181
184
  hasChunks:
@@ -715,8 +718,9 @@ export async function replayLogImpl(callApi: CallApiFn, args: ReplayLogArgs): Pr
715
718
  export type AddProviderInput = {
716
719
  name: string;
717
720
  apiKey: string;
718
- format: "anthropic" | "openai";
719
- model: string;
721
+ format?: "anthropic" | "openai";
722
+ model?: string;
723
+ models?: string[];
720
724
  anthropicBaseUrl?: string;
721
725
  openaiBaseUrl?: string;
722
726
  openaiResponsesBaseUrl?: string;
@@ -724,14 +728,23 @@ export type AddProviderInput = {
724
728
  apiDocsUrl?: string;
725
729
  };
726
730
 
731
+ function normalizedProviderModels(input: { model?: string; models?: readonly string[] }): string[] {
732
+ const modelList = input.models ?? [];
733
+ const models = modelList.map((model) => model.trim()).filter((model) => model !== "");
734
+ if (models.length > 0) return models;
735
+ const model = input.model?.trim();
736
+ return model !== undefined && model !== "" ? [model] : [];
737
+ }
738
+
727
739
  export async function addProviderImpl(
728
740
  callApi: CallApiFn,
729
741
  provider: AddProviderInput,
730
742
  ): Promise<ToolResult> {
743
+ const models = normalizedProviderModels(provider);
731
744
  const res = await callApi("/api/providers", {
732
745
  method: "POST",
733
746
  headers: { "content-type": "application/json" },
734
- body: JSON.stringify(provider),
747
+ body: JSON.stringify({ ...provider, models }),
735
748
  });
736
749
  if (!res.ok) return toolError(`POST /api/providers returned ${res.status}`);
737
750
  return textJson(await res.json());
@@ -743,6 +756,7 @@ export type UpdateProviderInput = {
743
756
  apiKey?: string;
744
757
  format?: "anthropic" | "openai";
745
758
  model?: string;
759
+ models?: string[];
746
760
  baseUrl?: string;
747
761
  authHeader?: "bearer" | "x-api-key";
748
762
  anthropicBaseUrl?: string;
@@ -756,10 +770,15 @@ export async function updateProviderImpl(
756
770
  input: UpdateProviderInput,
757
771
  ): Promise<ToolResult> {
758
772
  const { id, ...patch } = input;
773
+ const models =
774
+ patch.models !== undefined || patch.model !== undefined
775
+ ? normalizedProviderModels(patch)
776
+ : undefined;
777
+ const body = models === undefined ? patch : { ...patch, models };
759
778
  const res = await callApi(`/api/providers/${encodeURIComponent(id)}`, {
760
779
  method: "PUT",
761
780
  headers: { "content-type": "application/json" },
762
- body: JSON.stringify(patch),
781
+ body: JSON.stringify(body),
763
782
  });
764
783
  if (!res.ok) return toolError(`PUT /api/providers/${id} returned ${res.status}`);
765
784
  return textJson(await res.json());
@@ -14,10 +14,6 @@ export const alibabaProvider: ProviderAdapter = {
14
14
  matches(model: string): boolean {
15
15
  // Normalize: lowercase, replace whitespace with hyphens
16
16
  const m = model.toLowerCase().replace(/\s+/g, "-");
17
- // Match glm-5* and glm-5.1* models
18
- if (m.startsWith("glm-5")) {
19
- return true;
20
- }
21
17
  // Match qwen models (qwen3.6-plus, qwen3.7-max, Qwen3.6 Plus, etc.)
22
18
  if (m.startsWith("qwen")) {
23
19
  return true;