@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/CompareDrawer-BcEd6V-V.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +106 -0
- package/.output/public/assets/ReplayDialog-BNpC0548.js +1 -0
- package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-Ds1uRLVB.js} +1 -1
- package/.output/public/assets/ResponseView-7KPVqKl5.js +3 -0
- package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BHQT261s.js} +1 -1
- package/.output/public/assets/_sessionId-DWePGjnS.js +1 -0
- package/.output/public/assets/index-CI1-G8ua.js +1 -0
- package/.output/public/assets/index-DdhFqPsI.css +1 -0
- package/.output/public/assets/index-DjKt8XKe.js +14 -0
- package/.output/public/assets/json-viewer-CkCu-rka.js +1 -0
- package/.output/public/assets/{main-Dtspb4Ui.js → main-DpD1N0S8.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +194 -200
- package/.output/server/_libs/react-markdown.mjs +90 -2
- package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-DF9Sy8cP.mjs} +6 -57
- package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-BoxztaO7.mjs} +50 -74
- package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CRBkqFlJ.mjs} +1221 -799
- package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-Cc1dyDuK.mjs} +18 -62
- package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-CMGSsz5Z.mjs} +6 -57
- package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-Cp10DM1D.mjs} +139 -61
- package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-B1VGxy3A.mjs} +13 -58
- package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-47XVPghS.mjs} +6 -57
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/json-viewer-zDE2rrmJ.mjs +478 -0
- package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-DVeuZFqI.mjs} +680 -174
- package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Bp8JxtPW.mjs} +1 -1
- package/.output/server/index.mjs +72 -58
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +28 -2
- package/src/components/ProxyViewerContainer.tsx +25 -17
- package/src/components/clients/ClientLogo.tsx +132 -0
- package/src/components/groups/GroupsDialog.tsx +18 -13
- 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/AgentTraceSummary.tsx +56 -23
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
- package/src/components/proxy-viewer/CompareDrawer.tsx +58 -14
- package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
- package/src/components/proxy-viewer/ConversationHeader.tsx +33 -41
- package/src/components/proxy-viewer/LogEntry.tsx +41 -12
- package/src/components/proxy-viewer/LogEntryHeader.tsx +122 -51
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
- package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
- package/src/components/proxy-viewer/ThreadConnector.tsx +18 -7
- package/src/components/proxy-viewer/TurnGroup.tsx +67 -32
- 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 +77 -5
- 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/sessionInfoContract.ts +1 -0
- package/src/lib/stopReason.ts +59 -15
- 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/sessionInfo.ts +5 -1
- 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/ReplayDialog-sBA1KAYD.js +0 -1
- 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
|
@@ -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
|
}
|
|
@@ -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(),
|
package/src/lib/stopReason.ts
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
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: {
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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 {
|
|
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
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import type { FormatHandler, ParsedRequest } from "../handler";
|
|
2
2
|
import type { CapturedLog, TokenUsage } from "../../schemas";
|
|
3
|
-
import {
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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`
|
|
89
|
-
|
|
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 {
|
|
21
|
+
export type {
|
|
22
|
+
OpenAIResponse,
|
|
23
|
+
OpenAIResponsesRequest,
|
|
24
|
+
OpenAIResponsesResponse,
|
|
25
|
+
OpenAIResponsesSSEvent,
|
|
26
|
+
OpenAIResponsesOutputItem,
|
|
27
|
+
OpenAIToolCall,
|
|
28
|
+
} from "../../../contracts/openai";
|