@tonyclaw/agent-inspector 2.1.12 → 2.1.14

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 (83) hide show
  1. package/.output/cli.js +0 -8
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/CompareDrawer-DjgjIFx7.js +1 -0
  4. package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +106 -0
  5. package/.output/public/assets/{ReplayDialog-Cd1VsfRe.js → ReplayDialog-CU0Tbb2c.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-QxSpbmsj.js → RequestAnatomy-BNahe83D.js} +1 -1
  7. package/.output/public/assets/ResponseView-DSOnGqi6.js +3 -0
  8. package/.output/public/assets/{StreamingChunkSequence-CwNGqJ5n.js → StreamingChunkSequence-BEKTDklB.js} +1 -1
  9. package/.output/public/assets/_sessionId-Cif8JZdn.js +1 -0
  10. package/.output/public/assets/index-DWOkqdCa.js +14 -0
  11. package/.output/public/assets/index-D_WfwzUi.js +1 -0
  12. package/.output/public/assets/index-DtLuQrs0.css +1 -0
  13. package/.output/public/assets/json-viewer-C2JpgcW0.js +1 -0
  14. package/.output/public/assets/{main-B_EcGyf-.js → main-CSONBwwn.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +20 -20
  16. package/.output/server/_libs/react-markdown.mjs +90 -2
  17. package/.output/server/{_sessionId-CmO9read.mjs → _sessionId-CPAa37n5.mjs} +6 -57
  18. package/.output/server/_ssr/{CompareDrawer-D9wf870o.mjs → CompareDrawer-ceW5VxMo.mjs} +17 -62
  19. package/.output/server/_ssr/{ProxyViewerContainer-BDLJSawP.mjs → ProxyViewerContainer-CDfEE_w-.mjs} +872 -688
  20. package/.output/server/_ssr/{ReplayDialog-B2dS4kmP.mjs → ReplayDialog-V0s_eEbR.mjs} +8 -59
  21. package/.output/server/_ssr/{RequestAnatomy-k49Peshi.mjs → RequestAnatomy-f1ccwR9d.mjs} +6 -57
  22. package/.output/server/_ssr/{ResponseView-Bk3B1suu.mjs → ResponseView-BIRrqG4H.mjs} +138 -60
  23. package/.output/server/_ssr/{StreamingChunkSequence-D6MDGCgh.mjs → StreamingChunkSequence-V3JFjCgX.mjs} +13 -58
  24. package/.output/server/_ssr/{index-CC8KdlTb.mjs → index-DsykulzS.mjs} +6 -57
  25. package/.output/server/_ssr/index.mjs +2 -2
  26. package/.output/server/_ssr/json-viewer-Dcnm0Ivf.mjs +478 -0
  27. package/.output/server/_ssr/{router-BEmMnVa7.mjs → router-4bdm6Mt2.mjs} +674 -173
  28. package/.output/server/{_tanstack-start-manifest_v-Bf_4hG4w.mjs → _tanstack-start-manifest_v-Xp4CO64V.mjs} +1 -1
  29. package/.output/server/index.mjs +70 -56
  30. package/package.json +1 -1
  31. package/src/components/ProxyViewerContainer.tsx +25 -17
  32. package/src/components/providers/ImportWizardDialog.tsx +7 -1
  33. package/src/components/providers/ProviderCard.tsx +10 -1
  34. package/src/components/providers/ProviderForm.tsx +157 -41
  35. package/src/components/providers/ProvidersPanel.tsx +5 -1
  36. package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
  37. package/src/components/proxy-viewer/CompareDrawer.tsx +20 -6
  38. package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
  39. package/src/components/proxy-viewer/LogEntry.tsx +31 -11
  40. package/src/components/proxy-viewer/LogEntryHeader.tsx +111 -40
  41. package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
  42. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
  43. package/src/components/proxy-viewer/TurnGroup.tsx +53 -7
  44. package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
  45. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
  46. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
  47. package/src/components/proxy-viewer/formats/index.tsx +10 -3
  48. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
  49. package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
  50. package/src/components/proxy-viewer/logFocus.ts +15 -2
  51. package/src/components/proxy-viewer/requestTools.ts +7 -3
  52. package/src/components/proxy-viewer/viewerState.ts +72 -3
  53. package/src/components/ui/json-viewer-bulk.ts +41 -6
  54. package/src/components/ui/json-viewer.tsx +9 -8
  55. package/src/contracts/index.ts +15 -1
  56. package/src/contracts/openai.ts +94 -0
  57. package/src/lib/providerContract.ts +1 -0
  58. package/src/lib/providerModelMetadata.ts +7 -1
  59. package/src/lib/stopReason.ts +12 -0
  60. package/src/lib/upstreamUrl.ts +46 -0
  61. package/src/mcp/previewExtractor.ts +73 -3
  62. package/src/mcp/server.ts +2 -0
  63. package/src/mcp/toolHandlers.ts +2 -0
  64. package/src/proxy/constants.ts +2 -0
  65. package/src/proxy/formats/openai/handler.ts +40 -9
  66. package/src/proxy/formats/openai/index.ts +7 -0
  67. package/src/proxy/formats/openai/schemas.ts +15 -1
  68. package/src/proxy/formats/openai/stream.ts +529 -135
  69. package/src/proxy/formats/registry.ts +9 -1
  70. package/src/proxy/identityProxy.ts +0 -7
  71. package/src/proxy/providerImporters.ts +45 -5
  72. package/src/proxy/providers.ts +31 -11
  73. package/src/proxy/schemas.ts +6 -0
  74. package/src/proxy/toolSchemaWarnings.ts +25 -3
  75. package/src/proxy/upstream.ts +18 -37
  76. package/src/routes/api/providers.$providerId.ts +1 -0
  77. package/src/routes/api/providers.ts +2 -0
  78. package/.output/public/assets/CompareDrawer-XTdSOto9.js +0 -1
  79. package/.output/public/assets/ProxyViewerContainer-Cc944N8i.js +0 -117
  80. package/.output/public/assets/ResponseView-8GivXCzp.js +0 -1
  81. package/.output/public/assets/_sessionId-D89uDTQs.js +0 -1
  82. package/.output/public/assets/index-BsFdgXac.js +0 -1
  83. package/.output/public/assets/index-CmtfjQPv.css +0 -1
@@ -44,7 +44,29 @@ export type ToolTraceEvent = {
44
44
  argumentsPreview: string | null;
45
45
  };
46
46
 
47
+ type ToolTraceCacheEntry = {
48
+ apiFormat: CapturedLog["apiFormat"];
49
+ responseText: string | null;
50
+ events: ToolTraceEvent[];
51
+ };
52
+
47
53
  const PREVIEW_LIMIT = 180;
54
+ const toolTraceCache = new WeakMap<CapturedLog, ToolTraceCacheEntry>();
55
+ type ResolvedLogFormat = ReturnType<typeof resolveLogFormat>;
56
+
57
+ function responseMayContainToolTrace(log: CapturedLog, format: ResolvedLogFormat): boolean {
58
+ const responseText = log.responseText;
59
+ if (responseText === null) return false;
60
+
61
+ switch (format) {
62
+ case "anthropic":
63
+ return responseText.includes("tool_use");
64
+ case "openai":
65
+ return responseText.includes("tool_calls") || responseText.includes("function_call");
66
+ case "unknown":
67
+ return false;
68
+ }
69
+ }
48
70
 
49
71
  export function shouldRenderConversationContent(standalone: boolean, expanded: boolean): boolean {
50
72
  return standalone || expanded;
@@ -177,19 +199,66 @@ function extractOpenAIToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
177
199
  });
178
200
  }
179
201
  }
202
+ const output = safeGetOwnProperty(parsed, "output");
203
+ if (Array.isArray(output)) {
204
+ for (const item of output) {
205
+ if (safeGetOwnProperty(item, "type") !== "function_call") continue;
206
+ const name = safeGetOwnProperty(item, "name");
207
+ if (typeof name !== "string" || name.length === 0) continue;
208
+ const args = safeGetOwnProperty(item, "arguments");
209
+ events.push({
210
+ id: `${String(log.id)}-openai-tool-${String(events.length)}`,
211
+ logId: log.id,
212
+ index: events.length,
213
+ provider: "openai",
214
+ name,
215
+ argumentsText: copyValue(args),
216
+ argumentsPreview: previewValue(args),
217
+ });
218
+ }
219
+ }
180
220
  return events;
181
221
  }
182
222
 
183
223
  export function extractToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
224
+ const cached = toolTraceCache.get(log);
225
+ if (
226
+ cached !== undefined &&
227
+ cached.apiFormat === log.apiFormat &&
228
+ cached.responseText === log.responseText
229
+ ) {
230
+ return cached.events;
231
+ }
232
+
184
233
  const format = resolveLogFormat(log);
234
+ if (!responseMayContainToolTrace(log, format)) {
235
+ const events: ToolTraceEvent[] = [];
236
+ toolTraceCache.set(log, {
237
+ apiFormat: log.apiFormat,
238
+ responseText: log.responseText,
239
+ events,
240
+ });
241
+ return events;
242
+ }
243
+
244
+ let events: ToolTraceEvent[];
185
245
  switch (format) {
186
246
  case "anthropic":
187
- return extractAnthropicToolTraceEvents(log);
247
+ events = extractAnthropicToolTraceEvents(log);
248
+ break;
188
249
  case "openai":
189
- return extractOpenAIToolTraceEvents(log);
250
+ events = extractOpenAIToolTraceEvents(log);
251
+ break;
190
252
  case "unknown":
191
- return [];
253
+ events = [];
254
+ break;
192
255
  }
256
+ toolTraceCache.set(log, {
257
+ apiFormat: log.apiFormat,
258
+ responseText: log.responseText,
259
+ events,
260
+ });
261
+ return events;
193
262
  }
194
263
 
195
264
  export function buildTraceSummary(
@@ -1,17 +1,52 @@
1
1
  import { useCallback, useMemo, useState, useTransition } from "react";
2
2
 
3
3
  type JsonPrimitive = string | number | boolean | null;
4
- export type JsonValue =
5
- | JsonPrimitive
6
- | ReadonlyArray<JsonValue>
7
- | Readonly<{ [key: string]: JsonValue }>;
4
+ type JsonObject = Readonly<{ [key: string]: JsonValue }>;
5
+ export type JsonValue = JsonPrimitive | ReadonlyArray<JsonValue> | JsonObject;
8
6
 
9
7
  export type JsonExpansionPolicy = {
10
8
  depth: number;
11
9
  };
12
10
 
13
- export function getJsonExpansionPolicy(_value: JsonValue): JsonExpansionPolicy {
14
- return { depth: Number.POSITIVE_INFINITY };
11
+ const FULL_EXPAND_NODE_LIMIT = 400;
12
+ const STRUCTURE_EXPAND_NODE_LIMIT = 2_000;
13
+
14
+ function isJsonArray(value: JsonValue): value is ReadonlyArray<JsonValue> {
15
+ return Array.isArray(value);
16
+ }
17
+
18
+ function countJsonNodesUpTo(value: JsonValue, limit: number): number {
19
+ const stack: JsonValue[] = [value];
20
+ let count = 0;
21
+
22
+ while (stack.length > 0) {
23
+ const current = stack.pop();
24
+ if (current === undefined) continue;
25
+ count += 1;
26
+ if (count > limit) return count;
27
+
28
+ if (current === null || typeof current !== "object") continue;
29
+ if (isJsonArray(current)) {
30
+ for (const child of current) {
31
+ stack.push(child);
32
+ }
33
+ continue;
34
+ }
35
+
36
+ for (const key of Object.keys(current)) {
37
+ const child = current[key];
38
+ if (child !== undefined) stack.push(child);
39
+ }
40
+ }
41
+
42
+ return count;
43
+ }
44
+
45
+ export function getJsonExpansionPolicy(value: JsonValue): JsonExpansionPolicy {
46
+ const nodeCount = countJsonNodesUpTo(value, STRUCTURE_EXPAND_NODE_LIMIT + 1);
47
+ if (nodeCount <= FULL_EXPAND_NODE_LIMIT) return { depth: Number.POSITIVE_INFINITY };
48
+ if (nodeCount <= STRUCTURE_EXPAND_NODE_LIMIT) return { depth: 3 };
49
+ return { depth: 2 };
15
50
  }
16
51
 
17
52
  export type JsonBulkExpansion = {
@@ -1,6 +1,5 @@
1
1
  import { Check, ChevronDown, ChevronRight, ChevronsDown, Copy } from "lucide-react";
2
2
  import { type JSX, memo, useEffect, useMemo, useState } from "react";
3
- import ReactMarkdown from "react-markdown";
4
3
  import { copyTextToClipboard } from "../../lib/clipboard";
5
4
  import { cn } from "../../lib/utils";
6
5
  import { Button } from "./button";
@@ -46,6 +45,10 @@ function getEntries(value: JsonValue): ReadonlyArray<readonly [string, JsonValue
46
45
 
47
46
  const STRING_TRUNCATE_LIMIT = 120;
48
47
 
48
+ function JsonStringText({ text }: { text: string }): JSX.Element {
49
+ return <span className="whitespace-pre-wrap break-words">{text}</span>;
50
+ }
51
+
49
52
  function StringValue({ text }: { text: string }): JSX.Element {
50
53
  const [expanded, setExpanded] = useState(false);
51
54
  const isLong = text.length > STRING_TRUNCATE_LIMIT;
@@ -54,9 +57,7 @@ function StringValue({ text }: { text: string }): JSX.Element {
54
57
  return (
55
58
  <span className="text-emerald-400 break-all">
56
59
  &quot;
57
- <span className="prose prose-sm dark:prose-invert inline max-w-none [&_p]:inline [&_p]:my-0 [&_code]:text-emerald-300 [&_a]:text-emerald-300">
58
- <ReactMarkdown>{text}</ReactMarkdown>
59
- </span>
60
+ <JsonStringText text={text} />
60
61
  &quot;
61
62
  </span>
62
63
  );
@@ -67,7 +68,7 @@ function StringValue({ text }: { text: string }): JSX.Element {
67
68
  &quot;
68
69
  {expanded ? (
69
70
  <span
70
- className="cursor-pointer prose prose-sm dark:prose-invert inline max-w-none [&_p]:inline [&_p]:my-0 [&_code]:text-emerald-300 [&_a]:text-emerald-300"
71
+ className="cursor-pointer"
71
72
  onClick={(e) => {
72
73
  e.stopPropagation();
73
74
  setExpanded(false);
@@ -81,7 +82,7 @@ function StringValue({ text }: { text: string }): JSX.Element {
81
82
  role="button"
82
83
  tabIndex={0}
83
84
  >
84
- <ReactMarkdown>{text}</ReactMarkdown>
85
+ <JsonStringText text={text} />
85
86
  </span>
86
87
  ) : (
87
88
  <Tooltip delayDuration={300}>
@@ -93,14 +94,14 @@ function StringValue({ text }: { text: string }): JSX.Element {
93
94
  className="text-left cursor-pointer"
94
95
  >
95
96
  <span>{text.slice(0, STRING_TRUNCATE_LIMIT)}</span>
96
- <span className="text-emerald-400/50">…</span>
97
+ <span className="text-emerald-400/50">...</span>
97
98
  </TooltipTrigger>
98
99
  <TooltipContent
99
100
  side="bottom"
100
101
  className="max-w-md text-xs p-2 break-words whitespace-pre-wrap"
101
102
  >
102
103
  {text.slice(0, 500)}
103
- {text.length > 500 ? "" : ""}
104
+ {text.length > 500 ? "..." : ""}
104
105
  </TooltipContent>
105
106
  </Tooltip>
106
107
  )}
@@ -31,10 +31,24 @@ export {
31
31
  OpenAIMessage,
32
32
  OpenAIMessageContent,
33
33
  OpenAIRequestSchema,
34
+ OpenAIResponsesRequestSchema,
35
+ OpenAIResponsesResponseSchema,
36
+ OpenAIResponsesSSEventSchema,
37
+ OpenAIResponsesToolDefinition,
38
+ OpenAIResponsesUsageSchema,
39
+ OpenAIResponsesOutputItemSchema,
34
40
  OpenAIResponseSchema,
35
41
  OpenAISSERawChunkSchema,
36
42
  OpenAIToolCallSchema,
37
43
  OpenAIToolDefinition,
44
+ parseOpenAIResponsesResponse,
38
45
  parseOpenAIResponse,
39
46
  } from "./openai";
40
- export type { OpenAIResponse, OpenAIToolCall } from "./openai";
47
+ export type {
48
+ OpenAIResponse,
49
+ OpenAIResponsesRequest,
50
+ OpenAIResponsesResponse,
51
+ OpenAIResponsesSSEvent,
52
+ OpenAIResponsesOutputItem,
53
+ OpenAIToolCall,
54
+ } from "./openai";
@@ -75,6 +75,37 @@ export const OpenAIRequestSchema = z.object({
75
75
  user: z.string().optional(),
76
76
  });
77
77
 
78
+ export const OpenAIResponsesToolDefinition = z
79
+ .object({
80
+ type: z.string(),
81
+ name: z.string().optional(),
82
+ description: z.string().optional(),
83
+ parameters: JsonValueSchema.optional(),
84
+ function: z
85
+ .object({
86
+ name: z.string().optional(),
87
+ description: z.string().optional(),
88
+ parameters: JsonValueSchema.optional(),
89
+ })
90
+ .optional(),
91
+ })
92
+ .passthrough();
93
+
94
+ export const OpenAIResponsesRequestSchema = z
95
+ .object({
96
+ model: z.string(),
97
+ input: z.unknown().optional(),
98
+ instructions: z.union([z.string(), z.null()]).optional(),
99
+ stream: z.boolean().optional(),
100
+ tools: z.array(OpenAIResponsesToolDefinition).optional(),
101
+ tool_choice: z.unknown().optional(),
102
+ max_output_tokens: z.number().optional(),
103
+ temperature: z.number().optional(),
104
+ user: z.string().optional(),
105
+ metadata: z.record(z.string(), z.unknown()).optional(),
106
+ })
107
+ .passthrough();
108
+
78
109
  export const OpenAIChoiceDelta = z.object({
79
110
  role: z.enum(["assistant"]).optional(),
80
111
  content: z.string().nullable().optional(),
@@ -162,7 +193,59 @@ export const OpenAISSERawChunkSchema = z
162
193
  })
163
194
  .passthrough();
164
195
 
196
+ export const OpenAIResponsesUsageSchema = z
197
+ .object({
198
+ input_tokens: z.number().nullable().optional(),
199
+ output_tokens: z.number().nullable().optional(),
200
+ total_tokens: z.number().nullable().optional(),
201
+ input_tokens_details: z
202
+ .object({
203
+ cached_tokens: z.number().nullable().optional(),
204
+ })
205
+ .passthrough()
206
+ .nullable()
207
+ .optional(),
208
+ output_tokens_details: z
209
+ .object({
210
+ reasoning_tokens: z.number().nullable().optional(),
211
+ })
212
+ .passthrough()
213
+ .nullable()
214
+ .optional(),
215
+ })
216
+ .passthrough();
217
+
218
+ export const OpenAIResponsesOutputItemSchema = z
219
+ .object({
220
+ type: z.string(),
221
+ })
222
+ .passthrough();
223
+
224
+ export const OpenAIResponsesResponseSchema = z
225
+ .object({
226
+ id: z.string(),
227
+ object: z.literal("response").optional(),
228
+ created_at: z.number().optional(),
229
+ status: z.string().nullable().optional(),
230
+ model: z.string().optional(),
231
+ output: z.array(OpenAIResponsesOutputItemSchema).optional(),
232
+ output_text: z.string().optional(),
233
+ usage: OpenAIResponsesUsageSchema.nullable().optional(),
234
+ error: z.unknown().optional(),
235
+ })
236
+ .passthrough();
237
+
238
+ export const OpenAIResponsesSSEventSchema = z
239
+ .object({
240
+ type: z.string(),
241
+ })
242
+ .passthrough();
243
+
165
244
  export type OpenAIResponse = z.infer<typeof OpenAIResponseSchema>;
245
+ export type OpenAIResponsesRequest = z.infer<typeof OpenAIResponsesRequestSchema>;
246
+ export type OpenAIResponsesResponse = z.infer<typeof OpenAIResponsesResponseSchema>;
247
+ export type OpenAIResponsesSSEvent = z.infer<typeof OpenAIResponsesSSEventSchema>;
248
+ export type OpenAIResponsesOutputItem = z.infer<typeof OpenAIResponsesOutputItemSchema>;
166
249
 
167
250
  export function parseOpenAIResponse(rawBody: string): OpenAIResponse | null {
168
251
  try {
@@ -174,3 +257,14 @@ export function parseOpenAIResponse(rawBody: string): OpenAIResponse | null {
174
257
  return null;
175
258
  }
176
259
  }
260
+
261
+ export function parseOpenAIResponsesResponse(rawBody: string): OpenAIResponsesResponse | null {
262
+ try {
263
+ const json: unknown = JSON.parse(rawBody);
264
+ const result = OpenAIResponsesResponseSchema.safeParse(json);
265
+ if (result.success) return result.data;
266
+ return null;
267
+ } catch {
268
+ return null;
269
+ }
270
+ }
@@ -25,6 +25,7 @@ export const ProviderConfigSchema = z.object({
25
25
  baseUrl: z.string().optional(),
26
26
  anthropicBaseUrl: z.string().optional(),
27
27
  openaiBaseUrl: z.string().optional(),
28
+ openaiResponsesBaseUrl: z.string().optional(),
28
29
  authHeader: z.enum(["bearer", "x-api-key"]).optional().default("bearer"),
29
30
  apiDocsUrl: z.string().optional(),
30
31
  modelMetadataUrl: z.string().optional(),
@@ -81,7 +81,7 @@ export const BUILTIN_PROVIDER_MODEL_REGISTRY: ProviderModelRegistry = {
81
81
  {
82
82
  name: "MiniMax",
83
83
  aliases: ["minimax"],
84
- baseUrls: ["https://api.minimaxi.com/anthropic"],
84
+ baseUrls: ["https://api.minimaxi.com/anthropic", "https://api.minimaxi.com"],
85
85
  models: [
86
86
  {
87
87
  id: "MiniMax M3",
@@ -245,6 +245,12 @@ function providerBaseUrls(provider: ProviderConfig): string[] {
245
245
  if (provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl.trim() !== "") {
246
246
  urls.push(provider.openaiBaseUrl);
247
247
  }
248
+ if (
249
+ provider.openaiResponsesBaseUrl !== undefined &&
250
+ provider.openaiResponsesBaseUrl.trim() !== ""
251
+ ) {
252
+ urls.push(provider.openaiResponsesBaseUrl);
253
+ }
248
254
  if (provider.baseUrl !== undefined && provider.baseUrl.trim() !== "") {
249
255
  urls.push(provider.baseUrl);
250
256
  }
@@ -43,6 +43,18 @@ export function extractStopReason(log: CapturedLog): StopReason {
43
43
  return "stop";
44
44
  }
45
45
 
46
+ // OpenAI Responses shape: { status: "completed", output: [...] }
47
+ if (Array.isArray(json.output)) {
48
+ for (const item of json.output) {
49
+ if (isRecord(item) && item.type === "function_call") {
50
+ return "tool_use";
51
+ }
52
+ }
53
+ if (json.status === "completed") {
54
+ return "stop";
55
+ }
56
+ }
57
+
46
58
  return null;
47
59
  } catch {
48
60
  return null;
@@ -0,0 +1,46 @@
1
+ export const PATH_V1_CHAT_COMPLETIONS = "/v1/chat/completions";
2
+ export const PATH_V1_RESPONSES = "/v1/responses";
3
+
4
+ export function buildUpstreamUrl(upstreamBase: string, normalizedPath: string): string {
5
+ const base = upstreamBase.endsWith("/") ? upstreamBase.slice(0, -1) : upstreamBase;
6
+ const endpointPath = normalizedPath.split("?")[0] ?? normalizedPath;
7
+ const queryIndex = normalizedPath.indexOf("?");
8
+ const search = queryIndex >= 0 ? normalizedPath.slice(queryIndex) : "";
9
+
10
+ // Some providers expose a format-specific URL as the exact endpoint rather
11
+ // than a base URL. Keep existing configs with exact endpoints compatible.
12
+ if (endpointPath !== "" && base.endsWith(endpointPath)) {
13
+ return base + search;
14
+ }
15
+
16
+ if (isZhipuVersionedOpenAIBase(base) && isVersionedChatCompletionsPath(normalizedPath)) {
17
+ return base + normalizedPath.slice(3);
18
+ }
19
+
20
+ // Many OpenAI-compatible base URLs already include /v1. Avoid producing /v1/v1.
21
+ if (base.endsWith("/v1") && normalizedPath.startsWith("/v1/")) {
22
+ return base + normalizedPath.slice(3);
23
+ }
24
+ return base + normalizedPath;
25
+ }
26
+
27
+ function isVersionedChatCompletionsPath(normalizedPath: string): boolean {
28
+ return (
29
+ normalizedPath === PATH_V1_CHAT_COMPLETIONS ||
30
+ normalizedPath.startsWith(`${PATH_V1_CHAT_COMPLETIONS}?`) ||
31
+ normalizedPath.startsWith(`${PATH_V1_CHAT_COMPLETIONS}/`)
32
+ );
33
+ }
34
+
35
+ function isZhipuVersionedOpenAIBase(base: string): boolean {
36
+ try {
37
+ const parsed = new URL(base);
38
+ const path = parsed.pathname.replace(/\/+$/, "");
39
+ return (
40
+ parsed.hostname === "open.bigmodel.cn" &&
41
+ (path === "/api/paas/v4" || path === "/api/coding/paas/v4")
42
+ );
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
@@ -19,12 +19,20 @@
19
19
  * - the relevant content is non-text (e.g., image-only multimodal block)
20
20
  */
21
21
 
22
+ import { z } from "zod";
22
23
  import type { CapturedLog } from "../contracts";
23
24
  import { AnthropicRequestSchema, AnthropicResponseSchema } from "../contracts/anthropic";
24
- import { OpenAIRequestSchema, OpenAIResponseSchema } from "../contracts/openai";
25
+ import {
26
+ OpenAIRequestSchema,
27
+ OpenAIResponseSchema,
28
+ OpenAIResponsesRequestSchema,
29
+ OpenAIResponsesResponseSchema,
30
+ } from "../contracts/openai";
31
+ import { safeGetOwnProperty } from "../lib/objectUtils";
25
32
 
26
33
  /** Preview length cap, per design D5. */
27
34
  const PREVIEW_MAX_CHARS = 500;
35
+ const UnknownArraySchema = z.array(z.unknown());
28
36
 
29
37
  function truncate(text: string): string {
30
38
  return text.length <= PREVIEW_MAX_CHARS ? text : text.slice(0, PREVIEW_MAX_CHARS);
@@ -68,6 +76,58 @@ function firstOpenAIText(
68
76
  return null;
69
77
  }
70
78
 
79
+ function firstResponsesInputText(input: unknown): string | null {
80
+ if (typeof input === "string") return input.length > 0 ? input : null;
81
+ const parsedInput = UnknownArraySchema.safeParse(input);
82
+ if (!parsedInput.success) return null;
83
+ for (let i = parsedInput.data.length - 1; i >= 0; i--) {
84
+ const item = parsedInput.data[i];
85
+ const role = safeGetOwnProperty(item, "role");
86
+ if (role !== "user" && role !== undefined) continue;
87
+ const directText = safeGetOwnProperty(item, "text");
88
+ if (typeof directText === "string" && directText.length > 0) return directText;
89
+ const content = safeGetOwnProperty(item, "content");
90
+ if (typeof content === "string" && content.length > 0) return content;
91
+ if (Array.isArray(content)) {
92
+ for (const part of content) {
93
+ const type = safeGetOwnProperty(part, "type");
94
+ const text = safeGetOwnProperty(part, "text");
95
+ if (
96
+ (type === "input_text" || type === "text") &&
97
+ typeof text === "string" &&
98
+ text.length > 0
99
+ ) {
100
+ return text;
101
+ }
102
+ }
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ function firstResponsesOutputText(response: unknown): string | null {
109
+ const outputText = safeGetOwnProperty(response, "output_text");
110
+ if (typeof outputText === "string" && outputText.length > 0) return outputText;
111
+ const output = safeGetOwnProperty(response, "output");
112
+ if (!Array.isArray(output)) return null;
113
+ for (const item of output) {
114
+ const content = safeGetOwnProperty(item, "content");
115
+ if (!Array.isArray(content)) continue;
116
+ for (const part of content) {
117
+ const type = safeGetOwnProperty(part, "type");
118
+ const text = safeGetOwnProperty(part, "text");
119
+ if (
120
+ (type === "output_text" || type === "text") &&
121
+ typeof text === "string" &&
122
+ text.length > 0
123
+ ) {
124
+ return text;
125
+ }
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+
71
131
  /**
72
132
  * Extract a 500-char preview of the last `role: "user"` message's first text
73
133
  * block. Returns `null` for unknown format, parse failures, or non-text
@@ -100,7 +160,12 @@ export function extractLastUserMessagePreview(log: CapturedLog): string | null {
100
160
 
101
161
  if (log.apiFormat === "openai") {
102
162
  const parsed = OpenAIRequestSchema.safeParse(json);
103
- if (!parsed.success) return null;
163
+ if (!parsed.success) {
164
+ const responsesParsed = OpenAIResponsesRequestSchema.safeParse(json);
165
+ if (!responsesParsed.success) return null;
166
+ const text = firstResponsesInputText(responsesParsed.data.input);
167
+ return text === null ? null : truncate(text);
168
+ }
104
169
  for (let i = parsed.data.messages.length - 1; i >= 0; i--) {
105
170
  const msg = parsed.data.messages[i];
106
171
  if (msg && msg.role === "user") {
@@ -143,7 +208,12 @@ export function extractResponsePreview(log: CapturedLog): string | null {
143
208
 
144
209
  if (log.apiFormat === "openai") {
145
210
  const parsed = OpenAIResponseSchema.safeParse(json);
146
- if (!parsed.success) return null;
211
+ if (!parsed.success) {
212
+ const responsesParsed = OpenAIResponsesResponseSchema.safeParse(json);
213
+ if (!responsesParsed.success) return null;
214
+ const text = firstResponsesOutputText(responsesParsed.data);
215
+ return text === null ? null : truncate(text);
216
+ }
147
217
  // Pick the first choice that actually carries a text-like content.
148
218
  for (const choice of parsed.data.choices) {
149
219
  const msg = choice.message;
package/src/mcp/server.ts CHANGED
@@ -1057,6 +1057,7 @@ function registerTools(server: McpServer): void {
1057
1057
  model: z.string().min(1),
1058
1058
  anthropicBaseUrl: z.string().optional(),
1059
1059
  openaiBaseUrl: z.string().optional(),
1060
+ openaiResponsesBaseUrl: z.string().optional(),
1060
1061
  authHeader: z.enum(["bearer", "x-api-key"]).optional(),
1061
1062
  apiDocsUrl: z.string().optional(),
1062
1063
  }),
@@ -1081,6 +1082,7 @@ PATCH-style update: only the fields you supply are changed. Returns the updated
1081
1082
  authHeader: z.enum(["bearer", "x-api-key"]).optional(),
1082
1083
  anthropicBaseUrl: z.string().optional(),
1083
1084
  openaiBaseUrl: z.string().optional(),
1085
+ openaiResponsesBaseUrl: z.string().optional(),
1084
1086
  apiDocsUrl: z.string().optional(),
1085
1087
  }),
1086
1088
  },
@@ -719,6 +719,7 @@ export type AddProviderInput = {
719
719
  model: string;
720
720
  anthropicBaseUrl?: string;
721
721
  openaiBaseUrl?: string;
722
+ openaiResponsesBaseUrl?: string;
722
723
  authHeader?: "bearer" | "x-api-key";
723
724
  apiDocsUrl?: string;
724
725
  };
@@ -746,6 +747,7 @@ export type UpdateProviderInput = {
746
747
  authHeader?: "bearer" | "x-api-key";
747
748
  anthropicBaseUrl?: string;
748
749
  openaiBaseUrl?: string;
750
+ openaiResponsesBaseUrl?: string;
749
751
  apiDocsUrl?: string;
750
752
  };
751
753
 
@@ -9,6 +9,8 @@ export const PROXY_IDENTITY = process.env["PROXY_IDENTITY"] ?? "inspector9527@To
9
9
  // API paths
10
10
  export const PATH_V1_CHAT_COMPLETIONS = "/v1/chat/completions";
11
11
  export const PATH_CHAT_COMPLETIONS = "/chat/completions";
12
+ export const PATH_V1_RESPONSES = "/v1/responses";
13
+ export const PATH_RESPONSES = "/responses";
12
14
  export const PATH_V1_MESSAGES = "/v1/messages";
13
15
 
14
16
  // Header names