@tonyclaw/llm-inspector 1.14.1 → 1.14.2
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/index-DDrUlr6L.js +105 -0
- package/.output/public/assets/index-DOG5AdQ9.css +1 -0
- package/.output/public/assets/{main-BzK2SzIB.js → main-BElVT2p3.js} +3 -3
- package/.output/server/_ssr/{index-Cso39vJc.mjs → index-DmLit8Ad.mjs} +145 -112
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-B8X3GXM2.mjs → router-BoeSXWHG.mjs} +539 -286
- package/.output/server/{_tanstack-start-manifest_v-vO4aM6jK.mjs → _tanstack-start-manifest_v-B6idtbmL.mjs} +1 -1
- package/.output/server/index.mjs +21 -21
- package/package.json +1 -1
- package/src/components/providers/ProviderCard.tsx +111 -6
- package/src/components/providers/ProviderForm.tsx +70 -34
- package/src/components/providers/ProvidersPanel.tsx +3 -3
- package/src/lib/providerContract.ts +1 -0
- package/src/lib/providerTestContract.ts +8 -0
- package/src/proxy/providers.ts +119 -21
- package/src/routes/api/providers.$providerId.test.log.ts +293 -0
- package/src/routes/api/providers.$providerId.ts +2 -1
- package/src/routes/api/providers.ts +3 -2
- package/.output/public/assets/index-DEUddp_2.css +0 -1
- package/.output/public/assets/index-ax85pt2A.js +0 -105
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-
|
|
1
|
+
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-BElVT2p3.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-DDrUlr6L.js"] }, "/api/config": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.ts", "children": ["/api/config/paths"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/mcp": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/mcp.ts" }, "/api/models": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId", "/api/providers/export", "/api/providers/import"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/proxy/$.ts" }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.paths.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/test"] }, "/api/providers/export": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.export.ts" }, "/api/providers/import": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.import.ts" }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.ts", "children": ["/api/providers/$providerId/test/log"] }, "/api/providers/$providerId/test/log": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.log.ts" } }, "clientEntry": "/assets/main-BElVT2p3.js" });
|
|
2
2
|
export {
|
|
3
3
|
tsrStartManifest
|
|
4
4
|
};
|
package/.output/server/index.mjs
CHANGED
|
@@ -38,51 +38,51 @@ const assets = {
|
|
|
38
38
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
39
39
|
"type": "image/svg+xml",
|
|
40
40
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
41
|
-
"mtime": "2026-06-
|
|
41
|
+
"mtime": "2026-06-10T13:27:43.020Z",
|
|
42
42
|
"size": 5915,
|
|
43
43
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
44
44
|
},
|
|
45
45
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
46
46
|
"type": "image/jpeg",
|
|
47
47
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
48
|
-
"mtime": "2026-06-
|
|
48
|
+
"mtime": "2026-06-10T13:27:43.023Z",
|
|
49
49
|
"size": 6918,
|
|
50
50
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
51
51
|
},
|
|
52
|
-
"/assets/index-
|
|
52
|
+
"/assets/index-DOG5AdQ9.css": {
|
|
53
53
|
"type": "text/css; charset=utf-8",
|
|
54
|
-
"etag": '"
|
|
55
|
-
"mtime": "2026-06-
|
|
56
|
-
"size":
|
|
57
|
-
"path": "../public/assets/index-
|
|
58
|
-
},
|
|
59
|
-
"/assets/main-BzK2SzIB.js": {
|
|
60
|
-
"type": "text/javascript; charset=utf-8",
|
|
61
|
-
"etag": '"50599-SX7gD+cBJGiUJBAZdsWQTyO8AMw"',
|
|
62
|
-
"mtime": "2026-06-10T10:39:07.175Z",
|
|
63
|
-
"size": 329113,
|
|
64
|
-
"path": "../public/assets/main-BzK2SzIB.js"
|
|
54
|
+
"etag": '"142da-qXGGlxiu6GXrwGw4lzJEDsR446o"',
|
|
55
|
+
"mtime": "2026-06-10T13:27:43.023Z",
|
|
56
|
+
"size": 82650,
|
|
57
|
+
"path": "../public/assets/index-DOG5AdQ9.css"
|
|
65
58
|
},
|
|
66
59
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
67
60
|
"type": "image/svg+xml",
|
|
68
61
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
69
|
-
"mtime": "2026-06-
|
|
62
|
+
"mtime": "2026-06-10T13:27:43.023Z",
|
|
70
63
|
"size": 11256,
|
|
71
64
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
72
65
|
},
|
|
73
66
|
"/assets/qwen-CONDcHqt.png": {
|
|
74
67
|
"type": "image/png",
|
|
75
68
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
76
|
-
"mtime": "2026-06-
|
|
69
|
+
"mtime": "2026-06-10T13:27:43.023Z",
|
|
77
70
|
"size": 357059,
|
|
78
71
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
79
72
|
},
|
|
80
|
-
"/assets/
|
|
73
|
+
"/assets/main-BElVT2p3.js": {
|
|
74
|
+
"type": "text/javascript; charset=utf-8",
|
|
75
|
+
"etag": '"50599-c0Re5vg5+KI0J9dHMI2SM+wdp9A"',
|
|
76
|
+
"mtime": "2026-06-10T13:27:43.023Z",
|
|
77
|
+
"size": 329113,
|
|
78
|
+
"path": "../public/assets/main-BElVT2p3.js"
|
|
79
|
+
},
|
|
80
|
+
"/assets/index-DDrUlr6L.js": {
|
|
81
81
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"
|
|
83
|
-
"mtime": "2026-06-
|
|
84
|
-
"size":
|
|
85
|
-
"path": "../public/assets/index-
|
|
82
|
+
"etag": '"91fb4-sGd/w9UW0TGYuvQ5WfgvmwcNukI"',
|
|
83
|
+
"mtime": "2026-06-10T13:27:43.024Z",
|
|
84
|
+
"size": 597940,
|
|
85
|
+
"path": "../public/assets/index-DDrUlr6L.js"
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
function readAsset(id) {
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type JSX, type ReactNode, useState } from "react";
|
|
1
|
+
import { type JSX, type ReactNode, useCallback, useRef, useState } from "react";
|
|
2
2
|
import { Button } from "../ui/button";
|
|
3
3
|
import {
|
|
4
4
|
Eye,
|
|
@@ -169,6 +169,20 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
169
169
|
);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
function formatTimeAgo(isoString: string): string {
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
const then = new Date(isoString).getTime();
|
|
175
|
+
const diffMs = now - then;
|
|
176
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
177
|
+
if (diffSec < 60) return "just now";
|
|
178
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
179
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
180
|
+
const diffHr = Math.floor(diffMin / 60);
|
|
181
|
+
if (diffHr < 24) return `${diffHr}h ago`;
|
|
182
|
+
const diffDay = Math.floor(diffHr / 24);
|
|
183
|
+
return `${diffDay}d ago`;
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
export function ProviderCard({
|
|
173
187
|
provider,
|
|
174
188
|
testResults,
|
|
@@ -181,6 +195,32 @@ export function ProviderCard({
|
|
|
181
195
|
}: ProviderCardProps): JSX.Element {
|
|
182
196
|
const [showApiKey, setShowApiKey] = useState(false);
|
|
183
197
|
const [copied, setCopied] = useState(false);
|
|
198
|
+
const [showModelResults, setShowModelResults] = useState(false);
|
|
199
|
+
const hasLoggedRef = useRef<string | null>(null);
|
|
200
|
+
const lastTestedAtRef = useRef<string | undefined>(undefined);
|
|
201
|
+
|
|
202
|
+
// Reset log state when new test results arrive
|
|
203
|
+
if (testResults?.testedAt !== undefined && testResults.testedAt !== lastTestedAtRef.current) {
|
|
204
|
+
lastTestedAtRef.current = testResults.testedAt;
|
|
205
|
+
hasLoggedRef.current = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Call log API when user expands model test results for the first time
|
|
209
|
+
const handleToggleModelResults = useCallback(() => {
|
|
210
|
+
setShowModelResults((v) => {
|
|
211
|
+
const next = !v;
|
|
212
|
+
if (next && hasLoggedRef.current === null && testResults?.models !== undefined) {
|
|
213
|
+
hasLoggedRef.current = testResults.testedAt ?? "";
|
|
214
|
+
// Fire-and-forget: commit test results to dashboard log
|
|
215
|
+
void fetch(`/api/providers/${provider.id}/test/log`, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: { "Content-Type": "application/json" },
|
|
218
|
+
body: JSON.stringify(testResults),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return next;
|
|
222
|
+
});
|
|
223
|
+
}, [provider.id, testResults]);
|
|
184
224
|
|
|
185
225
|
function handleCopy() {
|
|
186
226
|
navigator.clipboard.writeText(provider.apiKey).catch(() => {});
|
|
@@ -201,11 +241,7 @@ export function ProviderCard({
|
|
|
201
241
|
>
|
|
202
242
|
<div className="flex items-start justify-between gap-2">
|
|
203
243
|
<div className="flex items-center gap-2 min-w-0">
|
|
204
|
-
<span className="font-medium truncate">
|
|
205
|
-
{provider.model !== undefined && provider.model !== ""
|
|
206
|
-
? `${provider.model} (${provider.name})`
|
|
207
|
-
: provider.name}
|
|
208
|
-
</span>
|
|
244
|
+
<span className="font-medium truncate">{provider.name}</span>
|
|
209
245
|
{provider.source === "company" && (
|
|
210
246
|
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 shrink-0">
|
|
211
247
|
公司
|
|
@@ -231,6 +267,16 @@ export function ProviderCard({
|
|
|
231
267
|
)}
|
|
232
268
|
</div>
|
|
233
269
|
|
|
270
|
+
{provider.models !== undefined && provider.models.length > 0 && (
|
|
271
|
+
<div className="flex flex-wrap gap-1">
|
|
272
|
+
{provider.models.map((m) => (
|
|
273
|
+
<span key={m} className="text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
|
|
274
|
+
{m}
|
|
275
|
+
</span>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
|
|
234
280
|
<div className="flex items-center gap-2">
|
|
235
281
|
<code className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded flex-1 truncate">
|
|
236
282
|
{showApiKey ? provider.apiKey : maskApiKey(provider.apiKey)}
|
|
@@ -273,6 +319,65 @@ export function ProviderCard({
|
|
|
273
319
|
</div>
|
|
274
320
|
)}
|
|
275
321
|
|
|
322
|
+
{testResults?.testedAt !== undefined && (
|
|
323
|
+
<div className="text-xs text-muted-foreground flex items-center gap-1">
|
|
324
|
+
<Clock className="size-3" />
|
|
325
|
+
<span>Tested {formatTimeAgo(testResults.testedAt)}</span>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
{testResults?.models !== undefined && Object.keys(testResults.models).length > 0 && (
|
|
329
|
+
<div className="border-t pt-2">
|
|
330
|
+
<button
|
|
331
|
+
type="button"
|
|
332
|
+
onClick={handleToggleModelResults}
|
|
333
|
+
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
|
334
|
+
>
|
|
335
|
+
<span className="font-mono">{showModelResults ? "▾" : "▸"}</span>
|
|
336
|
+
Model Test Results ({Object.keys(testResults.models).length})
|
|
337
|
+
</button>
|
|
338
|
+
{showModelResults && (
|
|
339
|
+
<div className="mt-2 overflow-x-auto">
|
|
340
|
+
<table className="w-full text-xs border-collapse">
|
|
341
|
+
<thead>
|
|
342
|
+
<tr className="border-b border-border">
|
|
343
|
+
<th className="text-left py-1 px-2 font-medium text-muted-foreground">Model</th>
|
|
344
|
+
{provider.anthropicBaseUrl !== undefined &&
|
|
345
|
+
provider.anthropicBaseUrl !== "" && (
|
|
346
|
+
<th className="text-left py-1 px-2 font-medium text-muted-foreground">
|
|
347
|
+
Anthropic
|
|
348
|
+
</th>
|
|
349
|
+
)}
|
|
350
|
+
{provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
|
|
351
|
+
<th className="text-left py-1 px-2 font-medium text-muted-foreground">
|
|
352
|
+
OpenAI
|
|
353
|
+
</th>
|
|
354
|
+
)}
|
|
355
|
+
</tr>
|
|
356
|
+
</thead>
|
|
357
|
+
<tbody>
|
|
358
|
+
{Object.entries(testResults.models).map(([modelName, modelResult]) => (
|
|
359
|
+
<tr key={modelName} className="border-b border-border/50 last:border-0">
|
|
360
|
+
<td className="py-1 px-2 font-medium">{modelName}</td>
|
|
361
|
+
{provider.anthropicBaseUrl !== undefined &&
|
|
362
|
+
provider.anthropicBaseUrl !== "" && (
|
|
363
|
+
<td className="py-1 px-2">
|
|
364
|
+
<TestStatus result={modelResult.anthropic.nonStreaming} />
|
|
365
|
+
</td>
|
|
366
|
+
)}
|
|
367
|
+
{provider.openaiBaseUrl !== undefined && provider.openaiBaseUrl !== "" && (
|
|
368
|
+
<td className="py-1 px-2">
|
|
369
|
+
<TestStatus result={modelResult.openai.nonStreaming} />
|
|
370
|
+
</td>
|
|
371
|
+
)}
|
|
372
|
+
</tr>
|
|
373
|
+
))}
|
|
374
|
+
</tbody>
|
|
375
|
+
</table>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
</div>
|
|
379
|
+
)}
|
|
380
|
+
|
|
276
381
|
<div className="flex gap-2 pt-1 border-t">
|
|
277
382
|
{onTest !== undefined && (
|
|
278
383
|
<Button
|
|
@@ -44,7 +44,7 @@ type ProviderFormProps = {
|
|
|
44
44
|
onSubmit: (data: {
|
|
45
45
|
name: string;
|
|
46
46
|
apiKey: string;
|
|
47
|
-
|
|
47
|
+
models: string[];
|
|
48
48
|
anthropicBaseUrl?: string;
|
|
49
49
|
openaiBaseUrl?: string;
|
|
50
50
|
apiDocsUrl?: string;
|
|
@@ -58,7 +58,10 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
58
58
|
const [apiKey, setApiKey] = useState(provider?.apiKey ?? "");
|
|
59
59
|
const [showApiKey, setShowApiKey] = useState(false);
|
|
60
60
|
const [copied, setCopied] = useState(false);
|
|
61
|
-
const
|
|
61
|
+
const initialModels = provider?.models;
|
|
62
|
+
const [models, setModels] = useState<string[]>(
|
|
63
|
+
initialModels !== undefined && initialModels.length > 0 ? initialModels : [""],
|
|
64
|
+
);
|
|
62
65
|
const [activeTab, setActiveTab] = useState<"anthropic" | "openai">("anthropic");
|
|
63
66
|
const [anthropicBaseUrl, setAnthropicBaseUrl] = useState(provider?.anthropicBaseUrl ?? "");
|
|
64
67
|
const [openaiBaseUrl, setOpenaiBaseUrl] = useState(provider?.openaiBaseUrl ?? "");
|
|
@@ -86,7 +89,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
86
89
|
if (provider) {
|
|
87
90
|
setName(provider.name);
|
|
88
91
|
setApiKey(provider.apiKey);
|
|
89
|
-
|
|
92
|
+
setModels((provider.models?.length ?? 0) > 0 ? provider.models : [""]);
|
|
90
93
|
setAnthropicBaseUrl(provider.anthropicBaseUrl ?? "");
|
|
91
94
|
setOpenaiBaseUrl(provider.openaiBaseUrl ?? "");
|
|
92
95
|
setApiDocsUrl(provider.apiDocsUrl ?? "");
|
|
@@ -110,12 +113,12 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
110
113
|
setApiDocsUrl(preset.apiDocsUrl);
|
|
111
114
|
}
|
|
112
115
|
// For MiniMax, auto-select the first model if not already set
|
|
113
|
-
if (keyword === "minimax" &&
|
|
114
|
-
|
|
116
|
+
if (keyword === "minimax" && models.length === 1 && models[0] === "") {
|
|
117
|
+
setModels([MINIMAX_MODELS[0] ?? ""]);
|
|
115
118
|
}
|
|
116
119
|
// For Alibaba, auto-select the first model if not already set
|
|
117
|
-
if (keyword === "alibaba" &&
|
|
118
|
-
|
|
120
|
+
if (keyword === "alibaba" && models.length === 1 && models[0] === "") {
|
|
121
|
+
setModels([ALIBABA_MODELS[0] ?? ""]);
|
|
119
122
|
}
|
|
120
123
|
break;
|
|
121
124
|
}
|
|
@@ -130,8 +133,8 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
130
133
|
if (!apiKey.trim()) {
|
|
131
134
|
newErrors.apiKey = "API key is required";
|
|
132
135
|
}
|
|
133
|
-
if (!
|
|
134
|
-
newErrors.
|
|
136
|
+
if (models.length === 0 || models.every((m) => !m.trim())) {
|
|
137
|
+
newErrors.models = "At least one model is required";
|
|
135
138
|
}
|
|
136
139
|
if (anthropicBaseUrl.trim() && !isValidUrl(anthropicBaseUrl.trim())) {
|
|
137
140
|
newErrors.anthropicBaseUrl = "Invalid URL format";
|
|
@@ -163,7 +166,7 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
163
166
|
onSubmit({
|
|
164
167
|
name: name.trim(),
|
|
165
168
|
apiKey: apiKey.trim(),
|
|
166
|
-
|
|
169
|
+
models: models.map((m) => m.trim()).filter((m) => m !== ""),
|
|
167
170
|
anthropicBaseUrl: anthropicBaseUrl.trim() || undefined,
|
|
168
171
|
openaiBaseUrl: openaiBaseUrl.trim() || undefined,
|
|
169
172
|
apiDocsUrl: apiDocsUrl.trim() || undefined,
|
|
@@ -254,33 +257,66 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
254
257
|
</div>
|
|
255
258
|
|
|
256
259
|
<div className="space-y-2">
|
|
257
|
-
<label
|
|
258
|
-
|
|
260
|
+
<label className="text-sm font-medium">
|
|
261
|
+
Models <span className="text-destructive">*</span>
|
|
259
262
|
</label>
|
|
260
|
-
{isMiniMax || isAlibaba
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
onChange={(e) => setModel(e.target.value)}
|
|
265
|
-
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
|
|
266
|
-
>
|
|
267
|
-
{(isMiniMax ? MINIMAX_MODELS : ALIBABA_MODELS).map((m) => (
|
|
268
|
-
<option key={m} value={m}>
|
|
269
|
-
{m}
|
|
270
|
-
</option>
|
|
263
|
+
{(isMiniMax || isAlibaba) && (
|
|
264
|
+
<datalist id="model-suggestions">
|
|
265
|
+
{(isMiniMax ? MINIMAX_MODELS : ALIBABA_MODELS).map((opt) => (
|
|
266
|
+
<option key={opt} value={opt} />
|
|
271
267
|
))}
|
|
272
|
-
</
|
|
273
|
-
) : (
|
|
274
|
-
<input
|
|
275
|
-
id="provider-model"
|
|
276
|
-
type="text"
|
|
277
|
-
value={model}
|
|
278
|
-
onChange={(e) => setModel(e.target.value)}
|
|
279
|
-
placeholder=""
|
|
280
|
-
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
|
|
281
|
-
/>
|
|
268
|
+
</datalist>
|
|
282
269
|
)}
|
|
283
|
-
{
|
|
270
|
+
{models.map((m, i) => (
|
|
271
|
+
<div key={i} className="flex items-center gap-2">
|
|
272
|
+
<input
|
|
273
|
+
type="text"
|
|
274
|
+
value={m}
|
|
275
|
+
onChange={(e) => {
|
|
276
|
+
setModels((prev) => {
|
|
277
|
+
const next = [...prev];
|
|
278
|
+
next[i] = e.target.value;
|
|
279
|
+
return next;
|
|
280
|
+
});
|
|
281
|
+
}}
|
|
282
|
+
placeholder={isMiniMax || isAlibaba ? "Type or select a model..." : "Model name"}
|
|
283
|
+
list={isMiniMax || isAlibaba ? "model-suggestions" : undefined}
|
|
284
|
+
className="flex-1 rounded-md border border-input bg-background px-4 py-3 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
|
|
285
|
+
/>
|
|
286
|
+
{models.length > 1 && (
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={() => setModels((prev) => prev.filter((_, idx) => idx !== i))}
|
|
290
|
+
className="text-muted-foreground hover:text-destructive transition-colors p-1 shrink-0"
|
|
291
|
+
aria-label="Remove model"
|
|
292
|
+
>
|
|
293
|
+
<svg
|
|
294
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
295
|
+
width="16"
|
|
296
|
+
height="16"
|
|
297
|
+
viewBox="0 0 24 24"
|
|
298
|
+
fill="none"
|
|
299
|
+
stroke="currentColor"
|
|
300
|
+
strokeWidth="2"
|
|
301
|
+
strokeLinecap="round"
|
|
302
|
+
strokeLinejoin="round"
|
|
303
|
+
>
|
|
304
|
+
<path d="M3 6h18" />
|
|
305
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
306
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
307
|
+
</svg>
|
|
308
|
+
</button>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
))}
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
onClick={() => setModels((prev) => [...prev, ""])}
|
|
315
|
+
className="text-xs text-primary hover:underline flex items-center gap-1"
|
|
316
|
+
>
|
|
317
|
+
+ Add Model
|
|
318
|
+
</button>
|
|
319
|
+
{errors.models !== undefined && <p className="text-xs text-destructive">{errors.models}</p>}
|
|
284
320
|
</div>
|
|
285
321
|
|
|
286
322
|
<div className="space-y-2">
|
|
@@ -32,7 +32,7 @@ const NETWORK_ERROR_MESSAGE = "Network error: could not reach the server. Is the
|
|
|
32
32
|
type ProviderFormData = {
|
|
33
33
|
name: string;
|
|
34
34
|
apiKey: string;
|
|
35
|
-
|
|
35
|
+
models: string[];
|
|
36
36
|
anthropicBaseUrl?: string;
|
|
37
37
|
openaiBaseUrl?: string;
|
|
38
38
|
apiDocsUrl?: string;
|
|
@@ -43,7 +43,7 @@ function createProviderPayload(data: ProviderFormData) {
|
|
|
43
43
|
return {
|
|
44
44
|
name: data.name,
|
|
45
45
|
apiKey: data.apiKey,
|
|
46
|
-
|
|
46
|
+
models: data.models,
|
|
47
47
|
anthropicBaseUrl: (data.anthropicBaseUrl?.length ?? 0) > 0 ? data.anthropicBaseUrl : undefined,
|
|
48
48
|
openaiBaseUrl: (data.openaiBaseUrl?.length ?? 0) > 0 ? data.openaiBaseUrl : undefined,
|
|
49
49
|
apiDocsUrl: (data.apiDocsUrl?.length ?? 0) > 0 ? data.apiDocsUrl : undefined,
|
|
@@ -285,7 +285,7 @@ export function ProvidersPanel({
|
|
|
285
285
|
// Only run connection test when critical fields (apiKey, model, base URLs) changed
|
|
286
286
|
const criticalFieldsChanged =
|
|
287
287
|
(data.apiKey ?? "") !== (editingProvider.apiKey ?? "") ||
|
|
288
|
-
(data.
|
|
288
|
+
JSON.stringify(data.models) !== JSON.stringify(editingProvider.models) ||
|
|
289
289
|
(data.anthropicBaseUrl ?? "") !== (editingProvider.anthropicBaseUrl ?? "") ||
|
|
290
290
|
(data.openaiBaseUrl ?? "") !== (editingProvider.openaiBaseUrl ?? "");
|
|
291
291
|
if (criticalFieldsChanged) {
|
|
@@ -11,6 +11,7 @@ export const ProviderConfigSchema = z.object({
|
|
|
11
11
|
name: z.string(),
|
|
12
12
|
apiKey: z.string(),
|
|
13
13
|
model: z.string().optional(),
|
|
14
|
+
models: z.array(z.string()).min(1),
|
|
14
15
|
format: z.enum(["anthropic", "openai"]).optional(),
|
|
15
16
|
baseUrl: z.string().optional(),
|
|
16
17
|
anthropicBaseUrl: z.string().optional(),
|
|
@@ -57,15 +57,23 @@ const ProviderFormatTestResultsSchema = z.object({
|
|
|
57
57
|
streaming: ProviderTestStateSchema,
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
const ModelTestResultsSchema = z.object({
|
|
61
|
+
anthropic: ProviderFormatTestResultsSchema,
|
|
62
|
+
openai: ProviderFormatTestResultsSchema,
|
|
63
|
+
});
|
|
64
|
+
|
|
60
65
|
export const ProviderTestResultsSchema = z.object({
|
|
61
66
|
anthropic: ProviderFormatTestResultsSchema,
|
|
62
67
|
openai: ProviderFormatTestResultsSchema,
|
|
68
|
+
models: z.record(z.string(), ModelTestResultsSchema).optional(),
|
|
69
|
+
testedAt: z.string().optional(),
|
|
63
70
|
});
|
|
64
71
|
|
|
65
72
|
export type ProviderTestErrorType = z.infer<typeof ProviderTestErrorTypeSchema>;
|
|
66
73
|
export type ProviderTestResult = z.infer<typeof ProviderTestResultSchema>;
|
|
67
74
|
export type ProviderTestState = z.infer<typeof ProviderTestStateSchema>;
|
|
68
75
|
export type ProviderTestResults = z.infer<typeof ProviderTestResultsSchema>;
|
|
76
|
+
export type ModelTestResults = z.infer<typeof ModelTestResultsSchema>;
|
|
69
77
|
|
|
70
78
|
export function createPendingProviderTestResults(): ProviderTestResults {
|
|
71
79
|
return {
|