@tonyclaw/agent-inspector 2.1.13 → 2.1.15

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 (92) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-BcEd6V-V.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +106 -0
  4. package/.output/public/assets/ReplayDialog-BNpC0548.js +1 -0
  5. package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-Ds1uRLVB.js} +1 -1
  6. package/.output/public/assets/ResponseView-7KPVqKl5.js +3 -0
  7. package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BHQT261s.js} +1 -1
  8. package/.output/public/assets/_sessionId-DWePGjnS.js +1 -0
  9. package/.output/public/assets/index-CI1-G8ua.js +1 -0
  10. package/.output/public/assets/index-DdhFqPsI.css +1 -0
  11. package/.output/public/assets/index-DjKt8XKe.js +14 -0
  12. package/.output/public/assets/json-viewer-CkCu-rka.js +1 -0
  13. package/.output/public/assets/{main-Dtspb4Ui.js → main-DpD1N0S8.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +194 -200
  15. package/.output/server/_libs/react-markdown.mjs +90 -2
  16. package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-DF9Sy8cP.mjs} +6 -57
  17. package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-BoxztaO7.mjs} +50 -74
  18. package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CRBkqFlJ.mjs} +1221 -799
  19. package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-Cc1dyDuK.mjs} +18 -62
  20. package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-CMGSsz5Z.mjs} +6 -57
  21. package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-Cp10DM1D.mjs} +139 -61
  22. package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-B1VGxy3A.mjs} +13 -58
  23. package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-47XVPghS.mjs} +6 -57
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/json-viewer-zDE2rrmJ.mjs +478 -0
  26. package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-DVeuZFqI.mjs} +680 -174
  27. package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Bp8JxtPW.mjs} +1 -1
  28. package/.output/server/index.mjs +72 -58
  29. package/package.json +1 -1
  30. package/src/components/ProxyViewer.tsx +28 -2
  31. package/src/components/ProxyViewerContainer.tsx +25 -17
  32. package/src/components/clients/ClientLogo.tsx +132 -0
  33. package/src/components/groups/GroupsDialog.tsx +18 -13
  34. package/src/components/providers/ImportWizardDialog.tsx +7 -1
  35. package/src/components/providers/ProviderCard.tsx +10 -1
  36. package/src/components/providers/ProviderForm.tsx +157 -41
  37. package/src/components/providers/ProvidersPanel.tsx +5 -1
  38. package/src/components/proxy-viewer/AgentTraceSummary.tsx +56 -23
  39. package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
  40. package/src/components/proxy-viewer/CompareDrawer.tsx +58 -14
  41. package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
  42. package/src/components/proxy-viewer/ConversationHeader.tsx +33 -41
  43. package/src/components/proxy-viewer/LogEntry.tsx +41 -12
  44. package/src/components/proxy-viewer/LogEntryHeader.tsx +122 -51
  45. package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
  46. package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
  47. package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
  48. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
  49. package/src/components/proxy-viewer/ThreadConnector.tsx +18 -7
  50. package/src/components/proxy-viewer/TurnGroup.tsx +67 -32
  51. package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
  52. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
  53. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
  54. package/src/components/proxy-viewer/formats/index.tsx +10 -3
  55. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
  56. package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
  57. package/src/components/proxy-viewer/logFocus.ts +15 -2
  58. package/src/components/proxy-viewer/requestTools.ts +7 -3
  59. package/src/components/proxy-viewer/viewerState.ts +77 -5
  60. package/src/components/ui/json-viewer-bulk.ts +41 -6
  61. package/src/components/ui/json-viewer.tsx +9 -8
  62. package/src/contracts/index.ts +15 -1
  63. package/src/contracts/openai.ts +94 -0
  64. package/src/lib/providerContract.ts +1 -0
  65. package/src/lib/providerModelMetadata.ts +7 -1
  66. package/src/lib/sessionInfoContract.ts +1 -0
  67. package/src/lib/stopReason.ts +59 -15
  68. package/src/lib/upstreamUrl.ts +46 -0
  69. package/src/mcp/previewExtractor.ts +73 -3
  70. package/src/mcp/server.ts +2 -0
  71. package/src/mcp/toolHandlers.ts +2 -0
  72. package/src/proxy/constants.ts +2 -0
  73. package/src/proxy/formats/openai/handler.ts +40 -9
  74. package/src/proxy/formats/openai/index.ts +7 -0
  75. package/src/proxy/formats/openai/schemas.ts +15 -1
  76. package/src/proxy/formats/openai/stream.ts +529 -135
  77. package/src/proxy/formats/registry.ts +9 -1
  78. package/src/proxy/providerImporters.ts +45 -5
  79. package/src/proxy/providers.ts +31 -11
  80. package/src/proxy/schemas.ts +6 -0
  81. package/src/proxy/sessionInfo.ts +5 -1
  82. package/src/proxy/toolSchemaWarnings.ts +25 -3
  83. package/src/proxy/upstream.ts +18 -37
  84. package/src/routes/api/providers.$providerId.ts +1 -0
  85. package/src/routes/api/providers.ts +2 -0
  86. package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
  87. package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
  88. package/.output/public/assets/ReplayDialog-sBA1KAYD.js +0 -1
  89. package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
  90. package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
  91. package/.output/public/assets/index-B-QQLbpz.js +0 -1
  92. package/.output/public/assets/index-CmtfjQPv.css +0 -1
@@ -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
  }
@@ -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(),
@@ -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,35 @@ 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;
59
+ return normalizeAnthropicStopReason(json.stop_reason);
60
+ }
61
+
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;
31
68
  }
32
- return null;
33
69
  }
34
70
 
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";
71
+ // OpenAI Responses shape: { status: "completed", output: [...] }
72
+ if (Array.isArray(json.output)) {
73
+ for (const item of json.output) {
74
+ if (isRecord(item) && item.type === "function_call") {
75
+ return "tool_use";
76
+ }
77
+ }
78
+ if (json.status === "completed") {
79
+ return "stop";
80
+ }
81
+ }
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
+ }
44
88
  }
45
89
 
46
90
  return null;
@@ -51,8 +95,8 @@ export function extractStopReason(log: CapturedLog): StopReason {
51
95
 
52
96
  /**
53
97
  * Returns true when the stop reason indicates the assistant completed its
54
- * turn naturally (Anthropic end_turn or OpenAI stop).
98
+ * turn, including responses truncated by output length limits.
55
99
  */
56
100
  export function isTurnBoundary(stopReason: StopReason): boolean {
57
- return stopReason === "end_turn" || stopReason === "stop";
101
+ return stopReason === "end_turn" || stopReason === "stop" || stopReason === "length";
58
102
  }
@@ -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
@@ -1,8 +1,22 @@
1
1
  import type { FormatHandler, ParsedRequest } from "../handler";
2
2
  import type { CapturedLog, TokenUsage } from "../../schemas";
3
- import { OpenAIRequestSchema, OpenAIResponseSchema, parseOpenAIResponse } from "./schemas";
3
+ import {
4
+ OpenAIRequestSchema,
5
+ OpenAIResponsesRequestSchema,
6
+ parseOpenAIResponsesResponse,
7
+ parseOpenAIResponse,
8
+ } from "./schemas";
4
9
  import { extractOpenAIStream } from "./stream";
5
10
 
11
+ function emptyTokens(): TokenUsage {
12
+ return {
13
+ inputTokens: null,
14
+ outputTokens: null,
15
+ cacheCreationInputTokens: null,
16
+ cacheReadInputTokens: null,
17
+ };
18
+ }
19
+
6
20
  export const OpenAIFormatHandler: FormatHandler = {
7
21
  format: "openai",
8
22
 
@@ -16,6 +30,13 @@ export const OpenAIFormatHandler: FormatHandler = {
16
30
  sessionId: headers?.get("x-session-affinity") ?? null,
17
31
  };
18
32
  }
33
+ const responsesResult = OpenAIResponsesRequestSchema.safeParse(json);
34
+ if (responsesResult.success) {
35
+ return {
36
+ model: responsesResult.data.model,
37
+ sessionId: headers?.get("x-session-affinity") ?? null,
38
+ };
39
+ }
19
40
  return null;
20
41
  } catch {
21
42
  return null;
@@ -62,12 +83,17 @@ export const OpenAIFormatHandler: FormatHandler = {
62
83
  cacheReadInputTokens,
63
84
  };
64
85
  }
65
- return {
66
- inputTokens: null,
67
- outputTokens: null,
68
- cacheCreationInputTokens: null,
69
- cacheReadInputTokens: null,
70
- };
86
+ const responsesParsed = parseOpenAIResponsesResponse(responseBody);
87
+ if (responsesParsed !== null) {
88
+ const usage = responsesParsed.usage;
89
+ return {
90
+ inputTokens: usage?.input_tokens ?? null,
91
+ outputTokens: usage?.output_tokens ?? null,
92
+ cacheCreationInputTokens: null,
93
+ cacheReadInputTokens: usage?.input_tokens_details?.cached_tokens ?? null,
94
+ };
95
+ }
96
+ return emptyTokens();
71
97
  },
72
98
 
73
99
  extractStream(
@@ -85,8 +111,13 @@ export const OpenAIFormatHandler: FormatHandler = {
85
111
  const json: unknown = JSON.parse(rawBody);
86
112
  if (typeof json === "object" && json !== null && !Array.isArray(json)) {
87
113
  const keys = Object.keys(json);
88
- // OpenAI has `model` and `messages` at the top level, but NOT `system`
89
- return keys.includes("model") && keys.includes("messages") && !keys.includes("system");
114
+ // OpenAI has `model` and either Chat `messages` or Responses `input`,
115
+ // but not Anthropic's top-level `system`.
116
+ return (
117
+ keys.includes("model") &&
118
+ (keys.includes("messages") || keys.includes("input")) &&
119
+ !keys.includes("system")
120
+ );
90
121
  }
91
122
  return false;
92
123
  } catch {
@@ -9,8 +9,15 @@ export {
9
9
  OpenAIMessage,
10
10
  OpenAIMessageContent,
11
11
  OpenAIToolDefinition,
12
+ OpenAIResponsesRequestSchema,
13
+ OpenAIResponsesResponseSchema,
14
+ OpenAIResponsesSSEventSchema,
15
+ OpenAIResponsesToolDefinition,
16
+ OpenAIResponsesUsageSchema,
17
+ OpenAIResponsesOutputItemSchema,
12
18
  OpenAIChoice,
13
19
  OpenAIChoiceDelta,
20
+ parseOpenAIResponsesResponse,
14
21
  parseOpenAIResponse,
15
22
  } from "./schemas";
16
23
 
@@ -5,10 +5,24 @@ export {
5
5
  OpenAIMessage,
6
6
  OpenAIMessageContent,
7
7
  OpenAIRequestSchema,
8
+ OpenAIResponsesRequestSchema,
9
+ OpenAIResponsesResponseSchema,
10
+ OpenAIResponsesSSEventSchema,
11
+ OpenAIResponsesToolDefinition,
12
+ OpenAIResponsesUsageSchema,
13
+ OpenAIResponsesOutputItemSchema,
8
14
  OpenAIResponseSchema,
9
15
  OpenAISSERawChunkSchema,
10
16
  OpenAIToolCallSchema,
11
17
  OpenAIToolDefinition,
18
+ parseOpenAIResponsesResponse,
12
19
  parseOpenAIResponse,
13
20
  } from "../../../contracts/openai";
14
- export type { OpenAIResponse, OpenAIToolCall } from "../../../contracts/openai";
21
+ export type {
22
+ OpenAIResponse,
23
+ OpenAIResponsesRequest,
24
+ OpenAIResponsesResponse,
25
+ OpenAIResponsesSSEvent,
26
+ OpenAIResponsesOutputItem,
27
+ OpenAIToolCall,
28
+ } from "../../../contracts/openai";