@tonyclaw/agent-inspector 2.1.15 → 2.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/cli.js +57 -13
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-B9sLBHw5.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
- package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-D9I9W9n3.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-DGXK_Rii.js} +1 -1
- package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-df_JzwsS.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-i3DM5IKL.js} +1 -1
- package/.output/public/assets/_sessionId-CTpCHdFh.js +1 -0
- package/.output/public/assets/index-Blqvndcn.js +1 -0
- package/.output/public/assets/index-Cs79WRQj.css +1 -0
- package/.output/public/assets/{index-DjKt8XKe.js → index-Dqc2r1ea.js} +1 -1
- package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-D3QWQlSB.js} +1 -1
- package/.output/public/assets/{main-DpD1N0S8.js → main-ZhxhOCmF.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +255 -201
- package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +2732 -1849
- package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-HQfrxjO1.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-QSlSD11e.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +3 -3
- package/.output/server/_ssr/{index-47XVPghS.mjs → index-D_EzfXaN.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
- package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-__o2wrfO.mjs} +505 -136
- package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/README.md +57 -2
- package/package.json +1 -1
- package/src/cli/startupOutput.ts +16 -0
- package/src/cli.ts +61 -15
- package/src/components/ProxyViewer.tsx +535 -163
- package/src/components/ProxyViewerContainer.tsx +77 -67
- package/src/components/alerts/AlertsDialog.tsx +2 -1
- package/src/components/clients/ClientLogo.tsx +1 -1
- package/src/components/groups/GroupsDialog.tsx +19 -1
- package/src/components/providers/ProviderCard.tsx +133 -32
- package/src/components/providers/ProviderForm.tsx +61 -29
- package/src/components/providers/ProviderLogo.tsx +6 -1
- package/src/components/providers/ProvidersPanel.tsx +7 -7
- package/src/components/providers/SettingsDialog.tsx +13 -6
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
- package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
- package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
- package/src/components/proxy-viewer/ConversationHeader.tsx +153 -120
- package/src/components/proxy-viewer/LogEntry.tsx +130 -43
- package/src/components/proxy-viewer/LogEntryHeader.tsx +357 -354
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
- package/src/components/proxy-viewer/ThreadConnector.tsx +102 -83
- package/src/components/proxy-viewer/TurnGroup.tsx +33 -69
- package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
- package/src/components/proxy-viewer/bodyHydration.ts +56 -0
- package/src/components/proxy-viewer/viewerState.ts +0 -4
- package/src/components/ui/icon-trigger.ts +2 -0
- package/src/components/ui/transient-toast.tsx +104 -0
- package/src/lib/providerTestContract.ts +56 -10
- package/src/lib/providerTestPrompt.ts +23 -1
- package/src/lib/sessionInfoContract.ts +4 -0
- package/src/lib/upstreamUrl.ts +105 -1
- package/src/mcp/server.ts +71 -27
- package/src/mcp/toolHandlers.ts +24 -5
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
- package/src/proxy/sessionInfo.ts +31 -2
- package/src/proxy/sessionSupervisor.ts +20 -1
- package/src/proxy/store.ts +1 -1
- package/src/routes/api/providers.$providerId.test.log.ts +93 -14
- package/styles/globals.css +180 -180
- package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
- package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
- package/.output/public/assets/index-CI1-G8ua.js +0 -1
- package/.output/public/assets/index-DdhFqPsI.css +0 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { AlertTriangle, CheckCircle2, X } from "lucide-react";
|
|
2
|
+
import { useCallback, useEffect, useState, type JSX } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
export const TRANSIENT_TOAST_AUTO_DISMISS_MS = 3000;
|
|
7
|
+
|
|
8
|
+
export type TransientToastTone = "error" | "success";
|
|
9
|
+
|
|
10
|
+
export type TransientToastInput = {
|
|
11
|
+
message: string;
|
|
12
|
+
tone?: TransientToastTone;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type TransientToastNotice = {
|
|
16
|
+
id: number;
|
|
17
|
+
message: string;
|
|
18
|
+
tone: TransientToastTone;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function createNextTransientToast(
|
|
22
|
+
previous: TransientToastNotice | null,
|
|
23
|
+
input: TransientToastInput,
|
|
24
|
+
): TransientToastNotice {
|
|
25
|
+
return {
|
|
26
|
+
id: (previous?.id ?? 0) + 1,
|
|
27
|
+
message: input.message,
|
|
28
|
+
tone: input.tone ?? "error",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useTransientToast(): {
|
|
33
|
+
notice: TransientToastNotice | null;
|
|
34
|
+
showToast: (input: TransientToastInput) => void;
|
|
35
|
+
dismissToast: () => void;
|
|
36
|
+
} {
|
|
37
|
+
const [notice, setNotice] = useState<TransientToastNotice | null>(null);
|
|
38
|
+
|
|
39
|
+
const showToast = useCallback((input: TransientToastInput) => {
|
|
40
|
+
setNotice((previous) => createNextTransientToast(previous, input));
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const dismissToast = useCallback(() => {
|
|
44
|
+
setNotice(null);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (notice === null) return undefined;
|
|
49
|
+
const timeout = window.setTimeout(() => {
|
|
50
|
+
setNotice(null);
|
|
51
|
+
}, TRANSIENT_TOAST_AUTO_DISMISS_MS);
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
window.clearTimeout(timeout);
|
|
55
|
+
};
|
|
56
|
+
}, [notice]);
|
|
57
|
+
|
|
58
|
+
return { notice, showToast, dismissToast };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function TransientToast({
|
|
62
|
+
notice,
|
|
63
|
+
onDismiss,
|
|
64
|
+
}: {
|
|
65
|
+
notice: TransientToastNotice | null;
|
|
66
|
+
onDismiss: () => void;
|
|
67
|
+
}): JSX.Element | null {
|
|
68
|
+
if (notice === null) return null;
|
|
69
|
+
|
|
70
|
+
const isError = notice.tone === "error";
|
|
71
|
+
const Icon = isError ? AlertTriangle : CheckCircle2;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
aria-atomic="true"
|
|
76
|
+
aria-live="polite"
|
|
77
|
+
className="pointer-events-none fixed top-4 right-4 z-[100] w-[min(calc(100vw-2rem),26rem)]"
|
|
78
|
+
role="status"
|
|
79
|
+
>
|
|
80
|
+
<div
|
|
81
|
+
key={notice.id}
|
|
82
|
+
className={cn(
|
|
83
|
+
"pointer-events-auto flex items-start gap-2 rounded-md border px-3 py-2 text-sm shadow-[0_18px_50px_rgba(0,0,0,0.45)] backdrop-blur",
|
|
84
|
+
isError
|
|
85
|
+
? "border-red-400/30 bg-[#16090b]/95 text-red-100"
|
|
86
|
+
: "border-cyan-300/25 bg-[#071318]/95 text-cyan-100",
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
<Icon
|
|
90
|
+
className={cn("mt-0.5 size-4 shrink-0", isError ? "text-red-300" : "text-cyan-200")}
|
|
91
|
+
/>
|
|
92
|
+
<div className="min-w-0 flex-1 break-words leading-5">{notice.message}</div>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
className="inline-flex size-6 shrink-0 items-center justify-center rounded-md text-current/70 transition-colors hover:bg-white/10 hover:text-current"
|
|
96
|
+
aria-label="Dismiss notification"
|
|
97
|
+
onClick={onDismiss}
|
|
98
|
+
>
|
|
99
|
+
<X className="size-3.5" />
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -72,22 +72,58 @@ const ProviderFormatTestResultsSchema = z.object({
|
|
|
72
72
|
streaming: ProviderTestStateSchema,
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
anthropic: ProviderFormatTestResultsSchema,
|
|
77
|
-
openai: ProviderFormatTestResultsSchema,
|
|
78
|
-
});
|
|
75
|
+
export type ProviderFormatTestResults = z.infer<typeof ProviderFormatTestResultsSchema>;
|
|
79
76
|
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
77
|
+
export function createNotConfiguredProviderFormatTestResults(): ProviderFormatTestResults {
|
|
78
|
+
return {
|
|
79
|
+
nonStreaming: { notConfigured: true },
|
|
80
|
+
streaming: { notConfigured: true },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ModelTestResultsSchema = z
|
|
85
|
+
.object({
|
|
86
|
+
anthropic: ProviderFormatTestResultsSchema,
|
|
87
|
+
openai: ProviderFormatTestResultsSchema.optional(),
|
|
88
|
+
openaiChat: ProviderFormatTestResultsSchema.optional(),
|
|
89
|
+
openaiResponses: ProviderFormatTestResultsSchema.optional(),
|
|
90
|
+
})
|
|
91
|
+
.transform((value) => {
|
|
92
|
+
const openaiChat =
|
|
93
|
+
value.openaiChat ?? value.openai ?? createNotConfiguredProviderFormatTestResults();
|
|
94
|
+
return {
|
|
95
|
+
...value,
|
|
96
|
+
openai: value.openai ?? openaiChat,
|
|
97
|
+
openaiChat,
|
|
98
|
+
openaiResponses: value.openaiResponses ?? createNotConfiguredProviderFormatTestResults(),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const ProviderTestResultsSchema = z
|
|
103
|
+
.object({
|
|
104
|
+
anthropic: ProviderFormatTestResultsSchema,
|
|
105
|
+
openai: ProviderFormatTestResultsSchema.optional(),
|
|
106
|
+
openaiChat: ProviderFormatTestResultsSchema.optional(),
|
|
107
|
+
openaiResponses: ProviderFormatTestResultsSchema.optional(),
|
|
108
|
+
models: z.record(z.string(), ModelTestResultsSchema).optional(),
|
|
109
|
+
testedAt: z.string().optional(),
|
|
110
|
+
})
|
|
111
|
+
.transform((value) => {
|
|
112
|
+
const openaiChat =
|
|
113
|
+
value.openaiChat ?? value.openai ?? createNotConfiguredProviderFormatTestResults();
|
|
114
|
+
return {
|
|
115
|
+
...value,
|
|
116
|
+
openai: value.openai ?? openaiChat,
|
|
117
|
+
openaiChat,
|
|
118
|
+
openaiResponses: value.openaiResponses ?? createNotConfiguredProviderFormatTestResults(),
|
|
119
|
+
};
|
|
120
|
+
});
|
|
86
121
|
|
|
87
122
|
export type ProviderTestErrorType = z.infer<typeof ProviderTestErrorTypeSchema>;
|
|
88
123
|
export type ProviderTestDebug = z.infer<typeof ProviderTestDebugSchema>;
|
|
89
124
|
export type ProviderTestResult = z.infer<typeof ProviderTestResultSchema>;
|
|
90
125
|
export type ProviderTestState = z.infer<typeof ProviderTestStateSchema>;
|
|
126
|
+
export type ProviderModelTestResults = z.infer<typeof ModelTestResultsSchema>;
|
|
91
127
|
export type ProviderTestResults = z.infer<typeof ProviderTestResultsSchema>;
|
|
92
128
|
|
|
93
129
|
export function createPendingProviderTestResults(): ProviderTestResults {
|
|
@@ -100,6 +136,14 @@ export function createPendingProviderTestResults(): ProviderTestResults {
|
|
|
100
136
|
nonStreaming: { testing: true },
|
|
101
137
|
streaming: { testing: true },
|
|
102
138
|
},
|
|
139
|
+
openaiChat: {
|
|
140
|
+
nonStreaming: { testing: true },
|
|
141
|
+
streaming: { testing: true },
|
|
142
|
+
},
|
|
143
|
+
openaiResponses: {
|
|
144
|
+
nonStreaming: { testing: true },
|
|
145
|
+
streaming: { testing: true },
|
|
146
|
+
},
|
|
103
147
|
};
|
|
104
148
|
}
|
|
105
149
|
|
|
@@ -118,5 +162,7 @@ export function createFailedProviderTestResults(
|
|
|
118
162
|
return {
|
|
119
163
|
anthropic: createFormatResult(),
|
|
120
164
|
openai: createFormatResult(),
|
|
165
|
+
openaiChat: createFormatResult(),
|
|
166
|
+
openaiResponses: createFormatResult(),
|
|
121
167
|
};
|
|
122
168
|
}
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
export type ProviderTestMode = "non-streaming" | "streaming";
|
|
2
|
+
export type ProviderTestApiShape = "chat" | "responses";
|
|
2
3
|
|
|
3
4
|
export type ProviderTestMessage = {
|
|
4
5
|
role: "user";
|
|
5
6
|
content: string;
|
|
6
7
|
};
|
|
7
8
|
|
|
8
|
-
export type
|
|
9
|
+
export type ProviderTestChatRequestBody = {
|
|
9
10
|
model: string;
|
|
10
11
|
messages: ProviderTestMessage[];
|
|
11
12
|
max_tokens: number;
|
|
12
13
|
stream: boolean;
|
|
13
14
|
};
|
|
14
15
|
|
|
16
|
+
export type ProviderTestResponsesRequestBody = {
|
|
17
|
+
model: string;
|
|
18
|
+
input: string;
|
|
19
|
+
max_output_tokens: number;
|
|
20
|
+
stream: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ProviderTestRequestBody =
|
|
24
|
+
| ProviderTestChatRequestBody
|
|
25
|
+
| ProviderTestResponsesRequestBody;
|
|
26
|
+
|
|
15
27
|
const MEMORY_PROBE_FACTS = [
|
|
16
28
|
"Project fact: Agent Inspector captures LLM traffic and turns it into Raw Trace, Session Episode, and Memory Candidate layers.",
|
|
17
29
|
"Procedure: before release, run bun test, npm run typecheck, npm run lint, npm run build, and openspec validate --all --strict.",
|
|
@@ -63,7 +75,17 @@ export function buildProviderTestMessages(mode: ProviderTestMode): ProviderTestM
|
|
|
63
75
|
export function buildProviderTestRequestBody(
|
|
64
76
|
model: string,
|
|
65
77
|
mode: ProviderTestMode,
|
|
78
|
+
apiShape: ProviderTestApiShape = "chat",
|
|
66
79
|
): ProviderTestRequestBody {
|
|
80
|
+
if (apiShape === "responses") {
|
|
81
|
+
return {
|
|
82
|
+
model,
|
|
83
|
+
input: buildProviderTestUserPrompt(mode),
|
|
84
|
+
max_output_tokens: maxTokensForMode(mode),
|
|
85
|
+
stream: mode === "streaming",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
const base = {
|
|
68
90
|
model,
|
|
69
91
|
messages: buildProviderTestMessages(mode),
|
|
@@ -32,6 +32,9 @@ export const SessionLogSummarySchema = z.object({
|
|
|
32
32
|
cacheRead: z.number().nullable(),
|
|
33
33
|
}),
|
|
34
34
|
sessionId: z.string().nullable(),
|
|
35
|
+
userAgent: z.string().nullable().optional(),
|
|
36
|
+
clientPid: z.number().nullable().optional(),
|
|
37
|
+
clientProjectFolder: z.string().nullable().optional(),
|
|
35
38
|
});
|
|
36
39
|
|
|
37
40
|
export const SessionInfoSchema = z.object({
|
|
@@ -56,6 +59,7 @@ export const SessionInfoSchema = z.object({
|
|
|
56
59
|
tokenUsage: SessionTokenUsageSchema,
|
|
57
60
|
lastLogId: z.number().nullable(),
|
|
58
61
|
lastModel: z.string().nullable(),
|
|
62
|
+
clientUserAgent: z.string().nullable().optional(),
|
|
59
63
|
clientPid: z.number().nullable(),
|
|
60
64
|
clientProjectFolder: z.string().nullable(),
|
|
61
65
|
inspectorPath: z.string(),
|
package/src/lib/upstreamUrl.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
export const PATH_V1_CHAT_COMPLETIONS = "/v1/chat/completions";
|
|
2
2
|
export const PATH_V1_RESPONSES = "/v1/responses";
|
|
3
|
+
export const PATH_V1_MESSAGES = "/v1/messages";
|
|
4
|
+
|
|
5
|
+
export type UpstreamUrlDiagnostic = {
|
|
6
|
+
severity: "info" | "warning";
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type UpstreamUrlPreviewOptions = {
|
|
11
|
+
providerName?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type UpstreamUrlPreview = {
|
|
15
|
+
finalUrl: string;
|
|
16
|
+
diagnostics: UpstreamUrlDiagnostic[];
|
|
17
|
+
};
|
|
3
18
|
|
|
4
19
|
export function buildUpstreamUrl(upstreamBase: string, normalizedPath: string): string {
|
|
5
20
|
const base = upstreamBase.endsWith("/") ? upstreamBase.slice(0, -1) : upstreamBase;
|
|
6
|
-
const endpointPath = normalizedPath
|
|
21
|
+
const endpointPath = endpointPathFor(normalizedPath);
|
|
7
22
|
const queryIndex = normalizedPath.indexOf("?");
|
|
8
23
|
const search = queryIndex >= 0 ? normalizedPath.slice(queryIndex) : "";
|
|
9
24
|
|
|
@@ -24,6 +39,62 @@ export function buildUpstreamUrl(upstreamBase: string, normalizedPath: string):
|
|
|
24
39
|
return base + normalizedPath;
|
|
25
40
|
}
|
|
26
41
|
|
|
42
|
+
export function previewUpstreamUrl(
|
|
43
|
+
upstreamBase: string,
|
|
44
|
+
normalizedPath: string,
|
|
45
|
+
options: UpstreamUrlPreviewOptions = {},
|
|
46
|
+
): UpstreamUrlPreview {
|
|
47
|
+
const base = upstreamBase.trim().replace(/\/+$/, "");
|
|
48
|
+
const endpointPath = endpointPathFor(normalizedPath);
|
|
49
|
+
const finalUrl = buildUpstreamUrl(base, normalizedPath);
|
|
50
|
+
const diagnostics: UpstreamUrlDiagnostic[] = [];
|
|
51
|
+
const exactEndpoint = endpointPath !== "" && base.endsWith(endpointPath);
|
|
52
|
+
|
|
53
|
+
if (exactEndpoint) {
|
|
54
|
+
diagnostics.push({
|
|
55
|
+
severity: "info",
|
|
56
|
+
message: "This already includes the exact endpoint; no duplicate suffix will be appended.",
|
|
57
|
+
});
|
|
58
|
+
} else if (isZhipuVersionedOpenAIBase(base) && isVersionedChatCompletionsPath(normalizedPath)) {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
severity: "info",
|
|
61
|
+
message: "Zhipu bases already include the API version; only /chat/completions is appended.",
|
|
62
|
+
});
|
|
63
|
+
} else if (base.endsWith("/v1") && normalizedPath.startsWith("/v1/")) {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
severity: "info",
|
|
66
|
+
message: "Base URL already ends with /v1; the suffix will not repeat /v1.",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isResponsesPath(endpointPath) && baseEndsWithChatEndpoint(base)) {
|
|
71
|
+
diagnostics.push({
|
|
72
|
+
severity: "warning",
|
|
73
|
+
message: "This looks like a Chat Completions endpoint, so Responses would append after it.",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isChatCompletionsPath(endpointPath) && baseEndsWithResponsesEndpoint(base)) {
|
|
78
|
+
diagnostics.push({
|
|
79
|
+
severity: "warning",
|
|
80
|
+
message: "This looks like a Responses endpoint, so Chat Completions would append after it.",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isResponsesPath(endpointPath) && isLikelyDeepSeekBase(base, options.providerName)) {
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
severity: "warning",
|
|
87
|
+
message: "DeepSeek's official API does not currently expose OpenAI Responses.",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { finalUrl, diagnostics };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function endpointPathFor(normalizedPath: string): string {
|
|
95
|
+
return normalizedPath.split("?")[0] ?? normalizedPath;
|
|
96
|
+
}
|
|
97
|
+
|
|
27
98
|
function isVersionedChatCompletionsPath(normalizedPath: string): boolean {
|
|
28
99
|
return (
|
|
29
100
|
normalizedPath === PATH_V1_CHAT_COMPLETIONS ||
|
|
@@ -32,6 +103,39 @@ function isVersionedChatCompletionsPath(normalizedPath: string): boolean {
|
|
|
32
103
|
);
|
|
33
104
|
}
|
|
34
105
|
|
|
106
|
+
function isChatCompletionsPath(endpointPath: string): boolean {
|
|
107
|
+
return (
|
|
108
|
+
endpointPath === PATH_V1_CHAT_COMPLETIONS ||
|
|
109
|
+
endpointPath.startsWith(`${PATH_V1_CHAT_COMPLETIONS}/`)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isResponsesPath(endpointPath: string): boolean {
|
|
114
|
+
return endpointPath === PATH_V1_RESPONSES || endpointPath.startsWith(`${PATH_V1_RESPONSES}/`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function baseEndsWithChatEndpoint(base: string): boolean {
|
|
118
|
+
const normalized = base.toLowerCase();
|
|
119
|
+
return normalized.endsWith("/chat/completions");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function baseEndsWithResponsesEndpoint(base: string): boolean {
|
|
123
|
+
const normalized = base.toLowerCase();
|
|
124
|
+
return normalized.endsWith("/responses");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isLikelyDeepSeekBase(base: string, providerName: string | undefined): boolean {
|
|
128
|
+
const provider = providerName?.toLowerCase();
|
|
129
|
+
if (provider !== undefined && provider.includes("deepseek")) return true;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const parsed = new URL(base);
|
|
133
|
+
return parsed.hostname.toLowerCase().includes("deepseek");
|
|
134
|
+
} catch {
|
|
135
|
+
return base.toLowerCase().includes("deepseek");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
35
139
|
function isZhipuVersionedOpenAIBase(base: string): boolean {
|
|
36
140
|
try {
|
|
37
141
|
const parsed = new URL(base);
|
package/src/mcp/server.ts
CHANGED
|
@@ -185,7 +185,7 @@ Parameters:
|
|
|
185
185
|
Returns: array of "thick summary" objects, each containing:
|
|
186
186
|
- id, timestamp, provider, model, apiFormat, method, path, status, isStreaming,
|
|
187
187
|
hasError, latencyMs, tokens { input, output, cacheCreate, cacheRead },
|
|
188
|
-
sessionId, clientPid,
|
|
188
|
+
sessionId, userAgent, clientPid, clientCwd, clientProjectFolder,
|
|
189
189
|
- lastUserMessagePreview (string|null, ≤500 chars) — first text block of the last user message, parsed via the format-specific schema (NOT a raw body slice)
|
|
190
190
|
- responsePreview (string|null, ≤500 chars) — first text block of the assistant response
|
|
191
191
|
- hasChunks, hasRawRequestBody (booleans)
|
|
@@ -195,6 +195,72 @@ Either preview field is null when the body is unknown format, unparseable, or th
|
|
|
195
195
|
const PROVIDER_WRITE_WARNING =
|
|
196
196
|
"⚠ This tool mutates provider configuration. Confirm with the user before invoking.";
|
|
197
197
|
|
|
198
|
+
const AddProviderInputSchema = z
|
|
199
|
+
.object({
|
|
200
|
+
name: z.string().min(1).describe("Provider display name."),
|
|
201
|
+
apiKey: z.string().min(1).describe("Plaintext provider API key."),
|
|
202
|
+
format: z
|
|
203
|
+
.enum(["anthropic", "openai"])
|
|
204
|
+
.optional()
|
|
205
|
+
.describe("Primary API family. Use openai for Chat Completions or Responses providers."),
|
|
206
|
+
model: z
|
|
207
|
+
.string()
|
|
208
|
+
.min(1)
|
|
209
|
+
.optional()
|
|
210
|
+
.describe("Legacy single-model input. Prefer models for new clients."),
|
|
211
|
+
models: z.array(z.string().min(1)).min(1).optional().describe("Provider model names."),
|
|
212
|
+
anthropicBaseUrl: z
|
|
213
|
+
.string()
|
|
214
|
+
.optional()
|
|
215
|
+
.describe("Anthropic-compatible base or exact /v1/messages endpoint."),
|
|
216
|
+
openaiBaseUrl: z
|
|
217
|
+
.string()
|
|
218
|
+
.optional()
|
|
219
|
+
.describe("OpenAI Chat Completions base or exact /v1/chat/completions endpoint."),
|
|
220
|
+
openaiResponsesBaseUrl: z
|
|
221
|
+
.string()
|
|
222
|
+
.optional()
|
|
223
|
+
.describe(
|
|
224
|
+
"OpenAI Responses base or exact /v1/responses endpoint. MiniMax default endpoint is https://api.minimaxi.com/v1/responses.",
|
|
225
|
+
),
|
|
226
|
+
authHeader: z.enum(["bearer", "x-api-key"]).optional(),
|
|
227
|
+
apiDocsUrl: z.string().optional(),
|
|
228
|
+
})
|
|
229
|
+
.refine(
|
|
230
|
+
(input) => input.model !== undefined || (input.models !== undefined && input.models.length > 0),
|
|
231
|
+
{ message: "Provide models or legacy model." },
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const UpdateProviderInputSchema = z.object({
|
|
235
|
+
id: z.string().describe("The provider id to update."),
|
|
236
|
+
name: z.string().min(1).optional(),
|
|
237
|
+
apiKey: z.string().min(1).optional(),
|
|
238
|
+
format: z.enum(["anthropic", "openai"]).optional(),
|
|
239
|
+
model: z.string().min(1).optional().describe("Legacy single-model input. Prefer models."),
|
|
240
|
+
models: z.array(z.string().min(1)).optional().describe("Replace provider model names."),
|
|
241
|
+
baseUrl: z
|
|
242
|
+
.string()
|
|
243
|
+
.min(1)
|
|
244
|
+
.optional()
|
|
245
|
+
.describe("Legacy route URL retained for old configs; prefer format-specific URL fields."),
|
|
246
|
+
authHeader: z.enum(["bearer", "x-api-key"]).optional(),
|
|
247
|
+
anthropicBaseUrl: z
|
|
248
|
+
.string()
|
|
249
|
+
.optional()
|
|
250
|
+
.describe("Anthropic-compatible base or exact /v1/messages endpoint."),
|
|
251
|
+
openaiBaseUrl: z
|
|
252
|
+
.string()
|
|
253
|
+
.optional()
|
|
254
|
+
.describe("OpenAI Chat Completions base or exact /v1/chat/completions endpoint."),
|
|
255
|
+
openaiResponsesBaseUrl: z
|
|
256
|
+
.string()
|
|
257
|
+
.optional()
|
|
258
|
+
.describe(
|
|
259
|
+
"OpenAI Responses base or exact /v1/responses endpoint. MiniMax default endpoint is https://api.minimaxi.com/v1/responses.",
|
|
260
|
+
),
|
|
261
|
+
apiDocsUrl: z.string().optional(),
|
|
262
|
+
});
|
|
263
|
+
|
|
198
264
|
const LogResourceListResponseSchema = z.object({
|
|
199
265
|
logs: z.array(z.object({ id: z.number().int().positive() })),
|
|
200
266
|
});
|
|
@@ -1049,18 +1115,8 @@ function registerTools(server: McpServer): void {
|
|
|
1049
1115
|
"inspector_add_provider",
|
|
1050
1116
|
{
|
|
1051
1117
|
title: "Add a new LLM provider",
|
|
1052
|
-
description: `${PROVIDER_WRITE_WARNING}\n\nPersists a new provider to <dataDir>/providers.json. Required fields: name, apiKey,
|
|
1053
|
-
inputSchema:
|
|
1054
|
-
name: z.string().min(1),
|
|
1055
|
-
apiKey: z.string().min(1),
|
|
1056
|
-
format: z.enum(["anthropic", "openai"]),
|
|
1057
|
-
model: z.string().min(1),
|
|
1058
|
-
anthropicBaseUrl: z.string().optional(),
|
|
1059
|
-
openaiBaseUrl: z.string().optional(),
|
|
1060
|
-
openaiResponsesBaseUrl: z.string().optional(),
|
|
1061
|
-
authHeader: z.enum(["bearer", "x-api-key"]).optional(),
|
|
1062
|
-
apiDocsUrl: z.string().optional(),
|
|
1063
|
-
}),
|
|
1118
|
+
description: `${PROVIDER_WRITE_WARNING}\n\nPersists a new provider to <dataDir>/providers.json. Required fields: name, apiKey, and models (or legacy model). Chat Completions and Responses can use different upstream URLs: set openaiBaseUrl for /v1/chat/completions and openaiResponsesBaseUrl for /v1/responses. The URL builder accepts either a base URL such as https://api.minimaxi.com or an exact endpoint such as https://api.minimaxi.com/v1/responses.`,
|
|
1119
|
+
inputSchema: AddProviderInputSchema,
|
|
1064
1120
|
},
|
|
1065
1121
|
(provider) => safeWriteCall("inspector_add_provider", () => addProviderImpl(callApi, provider)),
|
|
1066
1122
|
);
|
|
@@ -1071,20 +1127,8 @@ function registerTools(server: McpServer): void {
|
|
|
1071
1127
|
title: "Update an existing provider",
|
|
1072
1128
|
description: `${PROVIDER_WRITE_WARNING} Updating with nonsense values effectively soft-deletes a provider. Confirm with the user before invoking.
|
|
1073
1129
|
|
|
1074
|
-
PATCH-style update: only the fields you supply are changed.
|
|
1075
|
-
inputSchema:
|
|
1076
|
-
id: z.string().describe("The provider id to update."),
|
|
1077
|
-
name: z.string().min(1).optional(),
|
|
1078
|
-
apiKey: z.string().min(1).optional(),
|
|
1079
|
-
format: z.enum(["anthropic", "openai"]).optional(),
|
|
1080
|
-
model: z.string().min(1).optional(),
|
|
1081
|
-
baseUrl: z.string().min(1).optional(),
|
|
1082
|
-
authHeader: z.enum(["bearer", "x-api-key"]).optional(),
|
|
1083
|
-
anthropicBaseUrl: z.string().optional(),
|
|
1084
|
-
openaiBaseUrl: z.string().optional(),
|
|
1085
|
-
openaiResponsesBaseUrl: z.string().optional(),
|
|
1086
|
-
apiDocsUrl: z.string().optional(),
|
|
1087
|
-
}),
|
|
1130
|
+
PATCH-style update: only the fields you supply are changed. Use models to update the configured model list. For OpenAI-compatible providers, openaiBaseUrl targets /v1/chat/completions and openaiResponsesBaseUrl targets /v1/responses, and those endpoints may be different.`,
|
|
1131
|
+
inputSchema: UpdateProviderInputSchema,
|
|
1088
1132
|
},
|
|
1089
1133
|
(input) => safeWriteCall("inspector_update_provider", () => updateProviderImpl(callApi, input)),
|
|
1090
1134
|
);
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -175,7 +175,10 @@ export function buildLogSummary(log: CapturedLog) {
|
|
|
175
175
|
cacheRead: log.cacheReadInputTokens,
|
|
176
176
|
},
|
|
177
177
|
sessionId: log.sessionId,
|
|
178
|
+
userAgent: log.userAgent ?? null,
|
|
178
179
|
clientPid: log.clientPid ?? null,
|
|
180
|
+
clientCwd: log.clientCwd ?? null,
|
|
181
|
+
clientProjectFolder: log.clientProjectFolder ?? null,
|
|
179
182
|
lastUserMessagePreview: extractLastUserMessagePreview(log),
|
|
180
183
|
responsePreview: extractResponsePreview(log),
|
|
181
184
|
hasChunks:
|
|
@@ -715,8 +718,9 @@ export async function replayLogImpl(callApi: CallApiFn, args: ReplayLogArgs): Pr
|
|
|
715
718
|
export type AddProviderInput = {
|
|
716
719
|
name: string;
|
|
717
720
|
apiKey: string;
|
|
718
|
-
format
|
|
719
|
-
model
|
|
721
|
+
format?: "anthropic" | "openai";
|
|
722
|
+
model?: string;
|
|
723
|
+
models?: string[];
|
|
720
724
|
anthropicBaseUrl?: string;
|
|
721
725
|
openaiBaseUrl?: string;
|
|
722
726
|
openaiResponsesBaseUrl?: string;
|
|
@@ -724,14 +728,23 @@ export type AddProviderInput = {
|
|
|
724
728
|
apiDocsUrl?: string;
|
|
725
729
|
};
|
|
726
730
|
|
|
731
|
+
function normalizedProviderModels(input: { model?: string; models?: readonly string[] }): string[] {
|
|
732
|
+
const modelList = input.models ?? [];
|
|
733
|
+
const models = modelList.map((model) => model.trim()).filter((model) => model !== "");
|
|
734
|
+
if (models.length > 0) return models;
|
|
735
|
+
const model = input.model?.trim();
|
|
736
|
+
return model !== undefined && model !== "" ? [model] : [];
|
|
737
|
+
}
|
|
738
|
+
|
|
727
739
|
export async function addProviderImpl(
|
|
728
740
|
callApi: CallApiFn,
|
|
729
741
|
provider: AddProviderInput,
|
|
730
742
|
): Promise<ToolResult> {
|
|
743
|
+
const models = normalizedProviderModels(provider);
|
|
731
744
|
const res = await callApi("/api/providers", {
|
|
732
745
|
method: "POST",
|
|
733
746
|
headers: { "content-type": "application/json" },
|
|
734
|
-
body: JSON.stringify(provider),
|
|
747
|
+
body: JSON.stringify({ ...provider, models }),
|
|
735
748
|
});
|
|
736
749
|
if (!res.ok) return toolError(`POST /api/providers returned ${res.status}`);
|
|
737
750
|
return textJson(await res.json());
|
|
@@ -743,6 +756,7 @@ export type UpdateProviderInput = {
|
|
|
743
756
|
apiKey?: string;
|
|
744
757
|
format?: "anthropic" | "openai";
|
|
745
758
|
model?: string;
|
|
759
|
+
models?: string[];
|
|
746
760
|
baseUrl?: string;
|
|
747
761
|
authHeader?: "bearer" | "x-api-key";
|
|
748
762
|
anthropicBaseUrl?: string;
|
|
@@ -756,10 +770,15 @@ export async function updateProviderImpl(
|
|
|
756
770
|
input: UpdateProviderInput,
|
|
757
771
|
): Promise<ToolResult> {
|
|
758
772
|
const { id, ...patch } = input;
|
|
773
|
+
const models =
|
|
774
|
+
patch.models !== undefined || patch.model !== undefined
|
|
775
|
+
? normalizedProviderModels(patch)
|
|
776
|
+
: undefined;
|
|
777
|
+
const body = models === undefined ? patch : { ...patch, models };
|
|
759
778
|
const res = await callApi(`/api/providers/${encodeURIComponent(id)}`, {
|
|
760
779
|
method: "PUT",
|
|
761
780
|
headers: { "content-type": "application/json" },
|
|
762
|
-
body: JSON.stringify(
|
|
781
|
+
body: JSON.stringify(body),
|
|
763
782
|
});
|
|
764
783
|
if (!res.ok) return toolError(`PUT /api/providers/${id} returned ${res.status}`);
|
|
765
784
|
return textJson(await res.json());
|
|
@@ -888,7 +907,7 @@ function providerDiagnosticHints(result: unknown): string[] {
|
|
|
888
907
|
}
|
|
889
908
|
if (text.includes("nonstream") || text.includes("non-stream")) {
|
|
890
909
|
hints.push(
|
|
891
|
-
"The failure mentions non-streaming. Compare the
|
|
910
|
+
"The failure mentions non-streaming. Compare the Anthropic, OpenAI Chat, and OpenAI Responses non-streaming path and payload with the streaming request.",
|
|
892
911
|
);
|
|
893
912
|
}
|
|
894
913
|
return [...new Set(hints)];
|
|
@@ -14,10 +14,6 @@ export const alibabaProvider: ProviderAdapter = {
|
|
|
14
14
|
matches(model: string): boolean {
|
|
15
15
|
// Normalize: lowercase, replace whitespace with hyphens
|
|
16
16
|
const m = model.toLowerCase().replace(/\s+/g, "-");
|
|
17
|
-
// Match glm-5* and glm-5.1* models
|
|
18
|
-
if (m.startsWith("glm-5")) {
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
17
|
// Match qwen models (qwen3.6-plus, qwen3.7-max, Qwen3.6 Plus, etc.)
|
|
22
18
|
if (m.startsWith("qwen")) {
|
|
23
19
|
return true;
|
package/src/proxy/sessionInfo.ts
CHANGED
|
@@ -65,6 +65,9 @@ export function buildSessionLogSummary(log: CapturedLog): SessionLogSummary {
|
|
|
65
65
|
cacheRead: log.cacheReadInputTokens,
|
|
66
66
|
},
|
|
67
67
|
sessionId: normalizeString(log.sessionId),
|
|
68
|
+
userAgent: normalizeString(log.userAgent),
|
|
69
|
+
clientPid: log.clientPid ?? null,
|
|
70
|
+
clientProjectFolder: normalizeString(log.clientProjectFolder),
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -118,6 +121,30 @@ function collectSortedValues(values: readonly (string | null)[]): string[] {
|
|
|
118
121
|
return [...set].sort();
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
function firstSummaryUserAgent(summaries: readonly SessionLogSummary[]): string | null {
|
|
125
|
+
for (const summary of summaries) {
|
|
126
|
+
const value = normalizeString(summary.userAgent);
|
|
127
|
+
if (value !== null) return value;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function firstSummaryClientPid(summaries: readonly SessionLogSummary[]): number | null {
|
|
133
|
+
for (const summary of summaries) {
|
|
134
|
+
const value = summary.clientPid;
|
|
135
|
+
if (value !== null && value !== undefined) return value;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function firstSummaryProjectFolder(summaries: readonly SessionLogSummary[]): string | null {
|
|
141
|
+
for (const summary of summaries) {
|
|
142
|
+
const value = normalizeString(summary.clientProjectFolder);
|
|
143
|
+
if (value !== null) return value;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
function countCompletedLogs(summaries: readonly SessionLogSummary[]): number {
|
|
122
149
|
let count = 0;
|
|
123
150
|
for (const summary of summaries) {
|
|
@@ -215,8 +242,10 @@ export function buildSessionInfo(input: BuildSessionInfoInput): SessionInfo {
|
|
|
215
242
|
tokenUsage,
|
|
216
243
|
lastLogId: input.snapshot?.lastLogId ?? latestLog?.id ?? null,
|
|
217
244
|
lastModel: normalizeString(input.snapshot?.lastModel) ?? latestLog?.model ?? null,
|
|
218
|
-
|
|
219
|
-
|
|
245
|
+
clientUserAgent: firstSummaryUserAgent(summaries),
|
|
246
|
+
clientPid: input.snapshot?.clientPid ?? firstSummaryClientPid(summaries),
|
|
247
|
+
clientProjectFolder:
|
|
248
|
+
normalizeString(input.snapshot?.clientProjectFolder) ?? firstSummaryProjectFolder(summaries),
|
|
220
249
|
inspectorPath,
|
|
221
250
|
inspectorUrl: buildAbsoluteUrl(input.baseUrl, inspectorPath),
|
|
222
251
|
apiPath,
|