@tonyclaw/agent-inspector 2.1.13 → 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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/CompareDrawer-DjgjIFx7.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +106 -0
- package/.output/public/assets/{ReplayDialog-sBA1KAYD.js → ReplayDialog-CU0Tbb2c.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-BNahe83D.js} +1 -1
- package/.output/public/assets/ResponseView-DSOnGqi6.js +3 -0
- package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BEKTDklB.js} +1 -1
- package/.output/public/assets/_sessionId-Cif8JZdn.js +1 -0
- package/.output/public/assets/index-DWOkqdCa.js +14 -0
- package/.output/public/assets/index-D_WfwzUi.js +1 -0
- package/.output/public/assets/index-DtLuQrs0.css +1 -0
- package/.output/public/assets/json-viewer-C2JpgcW0.js +1 -0
- package/.output/public/assets/{main-Dtspb4Ui.js → main-CSONBwwn.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +20 -20
- package/.output/server/_libs/react-markdown.mjs +90 -2
- package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-CPAa37n5.mjs} +6 -57
- package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-ceW5VxMo.mjs} +17 -62
- package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CDfEE_w-.mjs} +872 -688
- package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-V0s_eEbR.mjs} +8 -59
- package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-f1ccwR9d.mjs} +6 -57
- package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-BIRrqG4H.mjs} +138 -60
- package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-V3JFjCgX.mjs} +13 -58
- package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-DsykulzS.mjs} +6 -57
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/json-viewer-Dcnm0Ivf.mjs +478 -0
- package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-4bdm6Mt2.mjs} +674 -173
- package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Xp4CO64V.mjs} +1 -1
- package/.output/server/index.mjs +76 -62
- package/package.json +1 -1
- package/src/components/ProxyViewerContainer.tsx +25 -17
- package/src/components/providers/ImportWizardDialog.tsx +7 -1
- package/src/components/providers/ProviderCard.tsx +10 -1
- package/src/components/providers/ProviderForm.tsx +157 -41
- package/src/components/providers/ProvidersPanel.tsx +5 -1
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
- package/src/components/proxy-viewer/CompareDrawer.tsx +20 -6
- package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
- package/src/components/proxy-viewer/LogEntry.tsx +31 -11
- package/src/components/proxy-viewer/LogEntryHeader.tsx +111 -40
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
- package/src/components/proxy-viewer/TurnGroup.tsx +53 -7
- package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
- package/src/components/proxy-viewer/formats/index.tsx +10 -3
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
- package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
- package/src/components/proxy-viewer/logFocus.ts +15 -2
- package/src/components/proxy-viewer/requestTools.ts +7 -3
- package/src/components/proxy-viewer/viewerState.ts +72 -3
- package/src/components/ui/json-viewer-bulk.ts +41 -6
- package/src/components/ui/json-viewer.tsx +9 -8
- package/src/contracts/index.ts +15 -1
- package/src/contracts/openai.ts +94 -0
- package/src/lib/providerContract.ts +1 -0
- package/src/lib/providerModelMetadata.ts +7 -1
- package/src/lib/stopReason.ts +12 -0
- package/src/lib/upstreamUrl.ts +46 -0
- package/src/mcp/previewExtractor.ts +73 -3
- package/src/mcp/server.ts +2 -0
- package/src/mcp/toolHandlers.ts +2 -0
- package/src/proxy/constants.ts +2 -0
- package/src/proxy/formats/openai/handler.ts +40 -9
- package/src/proxy/formats/openai/index.ts +7 -0
- package/src/proxy/formats/openai/schemas.ts +15 -1
- package/src/proxy/formats/openai/stream.ts +529 -135
- package/src/proxy/formats/registry.ts +9 -1
- package/src/proxy/providerImporters.ts +45 -5
- package/src/proxy/providers.ts +31 -11
- package/src/proxy/schemas.ts +6 -0
- package/src/proxy/toolSchemaWarnings.ts +25 -3
- package/src/proxy/upstream.ts +18 -37
- package/src/routes/api/providers.$providerId.ts +1 -0
- package/src/routes/api/providers.ts +2 -0
- package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
- package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
- package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
- package/.output/public/assets/index-B-QQLbpz.js +0 -1
- 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
|
-
|
|
247
|
+
events = extractAnthropicToolTraceEvents(log);
|
|
248
|
+
break;
|
|
188
249
|
case "openai":
|
|
189
|
-
|
|
250
|
+
events = extractOpenAIToolTraceEvents(log);
|
|
251
|
+
break;
|
|
190
252
|
case "unknown":
|
|
191
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
"
|
|
57
|
-
<
|
|
58
|
-
<ReactMarkdown>{text}</ReactMarkdown>
|
|
59
|
-
</span>
|
|
60
|
+
<JsonStringText text={text} />
|
|
60
61
|
"
|
|
61
62
|
</span>
|
|
62
63
|
);
|
|
@@ -67,7 +68,7 @@ function StringValue({ text }: { text: string }): JSX.Element {
|
|
|
67
68
|
"
|
|
68
69
|
{expanded ? (
|
|
69
70
|
<span
|
|
70
|
-
className="cursor-pointer
|
|
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
|
-
<
|
|
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"
|
|
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
|
)}
|
package/src/contracts/index.ts
CHANGED
|
@@ -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 {
|
|
47
|
+
export type {
|
|
48
|
+
OpenAIResponse,
|
|
49
|
+
OpenAIResponsesRequest,
|
|
50
|
+
OpenAIResponsesResponse,
|
|
51
|
+
OpenAIResponsesSSEvent,
|
|
52
|
+
OpenAIResponsesOutputItem,
|
|
53
|
+
OpenAIToolCall,
|
|
54
|
+
} from "./openai";
|
package/src/contracts/openai.ts
CHANGED
|
@@ -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
|
}
|
package/src/lib/stopReason.ts
CHANGED
|
@@ -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 {
|
|
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)
|
|
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)
|
|
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
|
},
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -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
|
|
package/src/proxy/constants.ts
CHANGED
|
@@ -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
|