@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.
Files changed (73) hide show
  1. package/.output/cli.js +57 -13
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-B9sLBHw5.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
  5. package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-D9I9W9n3.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-DGXK_Rii.js} +1 -1
  7. package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-df_JzwsS.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-BHQT261s.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-DjKt8XKe.js → index-Dqc2r1ea.js} +1 -1
  13. package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-D3QWQlSB.js} +1 -1
  14. package/.output/public/assets/{main-DpD1N0S8.js → main-ZhxhOCmF.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +255 -201
  16. package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
  17. package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +2732 -1849
  19. package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
  20. package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-HQfrxjO1.mjs} +3 -3
  21. package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-QSlSD11e.mjs} +3 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +3 -3
  23. package/.output/server/_ssr/{index-47XVPghS.mjs → index-D_EzfXaN.mjs} +2 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
  26. package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-__o2wrfO.mjs} +505 -136
  27. package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
  28. package/.output/server/index.mjs +63 -63
  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 +535 -163
  34. package/src/components/ProxyViewerContainer.tsx +77 -67
  35. package/src/components/alerts/AlertsDialog.tsx +2 -1
  36. package/src/components/clients/ClientLogo.tsx +1 -1
  37. package/src/components/groups/GroupsDialog.tsx +19 -1
  38. package/src/components/providers/ProviderCard.tsx +133 -32
  39. package/src/components/providers/ProviderForm.tsx +61 -29
  40. package/src/components/providers/ProviderLogo.tsx +6 -1
  41. package/src/components/providers/ProvidersPanel.tsx +7 -7
  42. package/src/components/providers/SettingsDialog.tsx +13 -6
  43. package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
  44. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  45. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  46. package/src/components/proxy-viewer/ConversationHeader.tsx +153 -120
  47. package/src/components/proxy-viewer/LogEntry.tsx +130 -43
  48. package/src/components/proxy-viewer/LogEntryHeader.tsx +357 -354
  49. package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
  50. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  51. package/src/components/proxy-viewer/ThreadConnector.tsx +102 -83
  52. package/src/components/proxy-viewer/TurnGroup.tsx +33 -69
  53. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  54. package/src/components/proxy-viewer/bodyHydration.ts +56 -0
  55. package/src/components/proxy-viewer/viewerState.ts +0 -4
  56. package/src/components/ui/icon-trigger.ts +2 -0
  57. package/src/components/ui/transient-toast.tsx +104 -0
  58. package/src/lib/providerTestContract.ts +56 -10
  59. package/src/lib/providerTestPrompt.ts +23 -1
  60. package/src/lib/sessionInfoContract.ts +4 -0
  61. package/src/lib/upstreamUrl.ts +105 -1
  62. package/src/mcp/server.ts +71 -27
  63. package/src/mcp/toolHandlers.ts +24 -5
  64. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  65. package/src/proxy/sessionInfo.ts +31 -2
  66. package/src/proxy/sessionSupervisor.ts +20 -1
  67. package/src/proxy/store.ts +1 -1
  68. package/src/routes/api/providers.$providerId.test.log.ts +93 -14
  69. package/styles/globals.css +180 -180
  70. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
  71. package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
  72. package/.output/public/assets/index-CI1-G8ua.js +0 -1
  73. package/.output/public/assets/index-DdhFqPsI.css +0 -1
@@ -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
+ }
@@ -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),
@@ -32,6 +32,9 @@ export const SessionLogSummarySchema = z.object({
32
32
  cacheRead: z.number().nullable(),
33
33
  }),
34
34
  sessionId: z.string().nullable(),
35
+ userAgent: z.string().nullable().optional(),
36
+ clientPid: z.number().nullable().optional(),
37
+ clientProjectFolder: z.string().nullable().optional(),
35
38
  });
36
39
 
37
40
  export const SessionInfoSchema = z.object({
@@ -56,6 +59,7 @@ export const SessionInfoSchema = z.object({
56
59
  tokenUsage: SessionTokenUsageSchema,
57
60
  lastLogId: z.number().nullable(),
58
61
  lastModel: z.string().nullable(),
62
+ clientUserAgent: z.string().nullable().optional(),
59
63
  clientPid: z.number().nullable(),
60
64
  clientProjectFolder: z.string().nullable(),
61
65
  inspectorPath: z.string(),
@@ -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.split("?")[0] ?? 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);
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());
@@ -888,7 +907,7 @@ function providerDiagnosticHints(result: unknown): string[] {
888
907
  }
889
908
  if (text.includes("nonstream") || text.includes("non-stream")) {
890
909
  hints.push(
891
- "The failure mentions non-streaming. Compare the OpenAI/Anthropic non-streaming path and payload with the streaming request.",
910
+ "The failure mentions non-streaming. Compare the Anthropic, OpenAI Chat, and OpenAI Responses non-streaming path and payload with the streaming request.",
892
911
  );
893
912
  }
894
913
  return [...new Set(hints)];
@@ -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;
@@ -65,6 +65,9 @@ export function buildSessionLogSummary(log: CapturedLog): SessionLogSummary {
65
65
  cacheRead: log.cacheReadInputTokens,
66
66
  },
67
67
  sessionId: normalizeString(log.sessionId),
68
+ userAgent: normalizeString(log.userAgent),
69
+ clientPid: log.clientPid ?? null,
70
+ clientProjectFolder: normalizeString(log.clientProjectFolder),
68
71
  };
69
72
  }
70
73
 
@@ -118,6 +121,30 @@ function collectSortedValues(values: readonly (string | null)[]): string[] {
118
121
  return [...set].sort();
119
122
  }
120
123
 
124
+ function firstSummaryUserAgent(summaries: readonly SessionLogSummary[]): string | null {
125
+ for (const summary of summaries) {
126
+ const value = normalizeString(summary.userAgent);
127
+ if (value !== null) return value;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ function firstSummaryClientPid(summaries: readonly SessionLogSummary[]): number | null {
133
+ for (const summary of summaries) {
134
+ const value = summary.clientPid;
135
+ if (value !== null && value !== undefined) return value;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ function firstSummaryProjectFolder(summaries: readonly SessionLogSummary[]): string | null {
141
+ for (const summary of summaries) {
142
+ const value = normalizeString(summary.clientProjectFolder);
143
+ if (value !== null) return value;
144
+ }
145
+ return null;
146
+ }
147
+
121
148
  function countCompletedLogs(summaries: readonly SessionLogSummary[]): number {
122
149
  let count = 0;
123
150
  for (const summary of summaries) {
@@ -215,8 +242,10 @@ export function buildSessionInfo(input: BuildSessionInfoInput): SessionInfo {
215
242
  tokenUsage,
216
243
  lastLogId: input.snapshot?.lastLogId ?? latestLog?.id ?? null,
217
244
  lastModel: normalizeString(input.snapshot?.lastModel) ?? latestLog?.model ?? null,
218
- clientPid: input.snapshot?.clientPid ?? null,
219
- clientProjectFolder: normalizeString(input.snapshot?.clientProjectFolder),
245
+ clientUserAgent: firstSummaryUserAgent(summaries),
246
+ clientPid: input.snapshot?.clientPid ?? firstSummaryClientPid(summaries),
247
+ clientProjectFolder:
248
+ normalizeString(input.snapshot?.clientProjectFolder) ?? firstSummaryProjectFolder(summaries),
220
249
  inspectorPath,
221
250
  inspectorUrl: buildAbsoluteUrl(input.baseUrl, inspectorPath),
222
251
  apiPath,