@tonyclaw/llm-inspector 1.7.0 → 1.7.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/cli.js +54 -1
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{index-s4lwsWvq.js → index-Bf_WGooQ.js} +18 -18
- package/.output/public/assets/{main-Cp8AM0Pa.js → main-CpIX1ZHy.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +115 -76
- package/.output/server/_ssr/{index-ByCLZu7J.mjs → index-BZkxgx8f.mjs} +66 -10
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-Bq_mxeNz.mjs → router-D7g2K6y6.mjs} +106 -34
- package/.output/server/{_tanstack-start-manifest_v-C4E0e9my.mjs → _tanstack-start-manifest_v-b6u6g-Cr.mjs} +1 -1
- package/.output/server/index.mjs +14 -14
- package/package.json +1 -1
- package/src/cli.ts +72 -1
- package/src/components/providers/ProviderCard.tsx +82 -7
- package/src/components/providers/ProvidersPanel.tsx +40 -7
|
@@ -65,7 +65,7 @@ function RootDocument({ children }) {
|
|
|
65
65
|
] })
|
|
66
66
|
] });
|
|
67
67
|
}
|
|
68
|
-
const $$splitComponentImporter = () => import("./index-
|
|
68
|
+
const $$splitComponentImporter = () => import("./index-BZkxgx8f.mjs");
|
|
69
69
|
const Route$d = createFileRoute("/")({
|
|
70
70
|
component: lazyRouteComponent($$splitComponentImporter, "component")
|
|
71
71
|
});
|
|
@@ -2074,6 +2074,77 @@ const Route$3 = createFileRoute("/api/config/paths")({
|
|
|
2074
2074
|
}
|
|
2075
2075
|
}
|
|
2076
2076
|
});
|
|
2077
|
+
const ERROR_HINTS = {
|
|
2078
|
+
timeout: "The request took too long. Check your network connection or try again.",
|
|
2079
|
+
network_unreachable: "Cannot reach the server. Verify the base URL and check your firewall settings.",
|
|
2080
|
+
connection_refused: "Connection was refused. Make sure the provider server is running and the URL is correct.",
|
|
2081
|
+
auth_failed: "Authentication failed. Check your API key is correct and has not expired.",
|
|
2082
|
+
rate_limited: "Too many requests. Wait a moment and try again, or check your API quota.",
|
|
2083
|
+
server_error: "The provider server encountered an error. Check their status page and try again later.",
|
|
2084
|
+
invalid_response: "Received an unexpected response. The provider may be experiencing issues.",
|
|
2085
|
+
unknown: "An unexpected error occurred. Check the details and try again."
|
|
2086
|
+
};
|
|
2087
|
+
const MAX_ERROR_DETAILS_LENGTH = 200;
|
|
2088
|
+
function classifyError(error, responseStatus) {
|
|
2089
|
+
const errorStr = String(error);
|
|
2090
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
2091
|
+
return { type: "timeout", details: "Request was aborted due to timeout" };
|
|
2092
|
+
}
|
|
2093
|
+
if (errorStr.includes("timeout") || errorStr.includes("timed out") || errorStr.includes("Timeout")) {
|
|
2094
|
+
return { type: "timeout", details: truncateErrorDetails(errorStr) };
|
|
2095
|
+
}
|
|
2096
|
+
if (errorStr.includes("ECONNREFUSED") || errorStr.includes("connection refused")) {
|
|
2097
|
+
return { type: "connection_refused", details: truncateErrorDetails(errorStr) };
|
|
2098
|
+
}
|
|
2099
|
+
if (errorStr.includes("ENOTFOUND") || errorStr.includes("getaddrinfo") || errorStr.includes("DNS") || errorStr.includes("network") || errorStr.includes("fetch failed")) {
|
|
2100
|
+
return { type: "network_unreachable", details: truncateErrorDetails(errorStr) };
|
|
2101
|
+
}
|
|
2102
|
+
if (responseStatus !== void 0) {
|
|
2103
|
+
if (responseStatus === 401 || responseStatus === 403) {
|
|
2104
|
+
return { type: "auth_failed", details: `HTTP ${responseStatus}` };
|
|
2105
|
+
}
|
|
2106
|
+
if (responseStatus === 429) {
|
|
2107
|
+
return { type: "rate_limited", details: `HTTP ${responseStatus}` };
|
|
2108
|
+
}
|
|
2109
|
+
if (responseStatus >= 500 && responseStatus < 600) {
|
|
2110
|
+
return { type: "server_error", details: `HTTP ${responseStatus}` };
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return { type: "unknown", details: truncateErrorDetails(errorStr) };
|
|
2114
|
+
}
|
|
2115
|
+
function truncateErrorDetails(details) {
|
|
2116
|
+
if (details.length <= MAX_ERROR_DETAILS_LENGTH) {
|
|
2117
|
+
return details;
|
|
2118
|
+
}
|
|
2119
|
+
return details.slice(0, MAX_ERROR_DETAILS_LENGTH) + "...";
|
|
2120
|
+
}
|
|
2121
|
+
function createErrorResult(error, latencyMs, streaming, responseStatus) {
|
|
2122
|
+
const { type, details } = classifyError(error, responseStatus);
|
|
2123
|
+
return {
|
|
2124
|
+
success: false,
|
|
2125
|
+
error: {
|
|
2126
|
+
message: getErrorMessage(type),
|
|
2127
|
+
type,
|
|
2128
|
+
details,
|
|
2129
|
+
hint: ERROR_HINTS[type]
|
|
2130
|
+
},
|
|
2131
|
+
latencyMs,
|
|
2132
|
+
streaming
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
function getErrorMessage(type) {
|
|
2136
|
+
const messages = {
|
|
2137
|
+
timeout: "Request timed out",
|
|
2138
|
+
network_unreachable: "Cannot reach host",
|
|
2139
|
+
connection_refused: "Connection refused",
|
|
2140
|
+
auth_failed: "Authentication failed",
|
|
2141
|
+
rate_limited: "Rate limited",
|
|
2142
|
+
server_error: "Provider server error",
|
|
2143
|
+
invalid_response: "Invalid response",
|
|
2144
|
+
unknown: "Unknown error"
|
|
2145
|
+
};
|
|
2146
|
+
return messages[type];
|
|
2147
|
+
}
|
|
2077
2148
|
const AnthropicResponseSchema = object({
|
|
2078
2149
|
id: string().optional(),
|
|
2079
2150
|
type: string().optional(),
|
|
@@ -2106,6 +2177,7 @@ const OpenAIResponseSchema = object({
|
|
|
2106
2177
|
})
|
|
2107
2178
|
)
|
|
2108
2179
|
});
|
|
2180
|
+
const TEST_TIMEOUT_MS = 3e4;
|
|
2109
2181
|
async function testEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
|
|
2110
2182
|
const startTime = Date.now();
|
|
2111
2183
|
const body = JSON.stringify({
|
|
@@ -2113,6 +2185,8 @@ async function testEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
|
|
|
2113
2185
|
messages: [{ role: "user", content: "say hello and briefly introduce yourself" }],
|
|
2114
2186
|
max_tokens: 1024
|
|
2115
2187
|
});
|
|
2188
|
+
const controller = new AbortController();
|
|
2189
|
+
const timeoutId = setTimeout(() => controller.abort(), TEST_TIMEOUT_MS);
|
|
2116
2190
|
try {
|
|
2117
2191
|
const url = `${baseUrl}${path2}`;
|
|
2118
2192
|
const response = await fetch(url, {
|
|
@@ -2121,17 +2195,20 @@ async function testEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
|
|
|
2121
2195
|
"Content-Type": "application/json",
|
|
2122
2196
|
Authorization: `Bearer ${apiKey}`
|
|
2123
2197
|
},
|
|
2124
|
-
body
|
|
2198
|
+
body,
|
|
2199
|
+
signal: controller.signal
|
|
2125
2200
|
});
|
|
2201
|
+
clearTimeout(timeoutId);
|
|
2126
2202
|
const latencyMs = Date.now() - startTime;
|
|
2127
|
-
const responseText = await response.text();
|
|
2128
2203
|
if (!response.ok) {
|
|
2129
|
-
return
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2204
|
+
return createErrorResult(
|
|
2205
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
2206
|
+
latencyMs,
|
|
2207
|
+
false,
|
|
2208
|
+
response.status
|
|
2209
|
+
);
|
|
2134
2210
|
}
|
|
2211
|
+
const responseText = await response.text();
|
|
2135
2212
|
try {
|
|
2136
2213
|
if (isOpenAI) {
|
|
2137
2214
|
const json = OpenAIResponseSchema.parse(JSON.parse(responseText));
|
|
@@ -2181,11 +2258,8 @@ async function testEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
|
|
|
2181
2258
|
};
|
|
2182
2259
|
}
|
|
2183
2260
|
} catch (err) {
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
error: String(err),
|
|
2187
|
-
latencyMs: Date.now() - startTime
|
|
2188
|
-
};
|
|
2261
|
+
clearTimeout(timeoutId);
|
|
2262
|
+
return createErrorResult(err, Date.now() - startTime, false);
|
|
2189
2263
|
}
|
|
2190
2264
|
}
|
|
2191
2265
|
async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
|
|
@@ -2196,6 +2270,8 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
|
|
|
2196
2270
|
max_tokens: 256,
|
|
2197
2271
|
stream: true
|
|
2198
2272
|
});
|
|
2273
|
+
const controller = new AbortController();
|
|
2274
|
+
const timeoutId = setTimeout(() => controller.abort(), TEST_TIMEOUT_MS);
|
|
2199
2275
|
try {
|
|
2200
2276
|
const url = `${baseUrl}${path2}`;
|
|
2201
2277
|
const response = await fetch(url, {
|
|
@@ -2204,24 +2280,29 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
|
|
|
2204
2280
|
"Content-Type": "application/json",
|
|
2205
2281
|
Authorization: `Bearer ${apiKey}`
|
|
2206
2282
|
},
|
|
2207
|
-
body
|
|
2283
|
+
body,
|
|
2284
|
+
signal: controller.signal
|
|
2208
2285
|
});
|
|
2286
|
+
clearTimeout(timeoutId);
|
|
2209
2287
|
const latencyMs = Date.now() - startTime;
|
|
2210
2288
|
if (!response.ok) {
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
success: false,
|
|
2214
|
-
error: `HTTP ${response.status}: ${responseText.slice(0, 200)}`,
|
|
2289
|
+
return createErrorResult(
|
|
2290
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
2215
2291
|
latencyMs,
|
|
2216
|
-
|
|
2217
|
-
|
|
2292
|
+
true,
|
|
2293
|
+
response.status
|
|
2294
|
+
);
|
|
2218
2295
|
}
|
|
2219
2296
|
const chunks = [];
|
|
2220
2297
|
const reader = response.body?.getReader();
|
|
2221
2298
|
if (!reader) {
|
|
2222
2299
|
return {
|
|
2223
2300
|
success: false,
|
|
2224
|
-
error:
|
|
2301
|
+
error: {
|
|
2302
|
+
message: "No response body",
|
|
2303
|
+
type: "invalid_response",
|
|
2304
|
+
hint: ERROR_HINTS.invalid_response
|
|
2305
|
+
},
|
|
2225
2306
|
latencyMs,
|
|
2226
2307
|
streaming: true
|
|
2227
2308
|
};
|
|
@@ -2234,12 +2315,7 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
|
|
|
2234
2315
|
chunks.push(decoder.decode(value, { stream: true }));
|
|
2235
2316
|
}
|
|
2236
2317
|
} catch (readErr) {
|
|
2237
|
-
return {
|
|
2238
|
-
success: false,
|
|
2239
|
-
error: `Stream read error: ${readErr}`,
|
|
2240
|
-
latencyMs,
|
|
2241
|
-
streaming: true
|
|
2242
|
-
};
|
|
2318
|
+
return createErrorResult(`Stream read error: ${readErr}`, latencyMs, true);
|
|
2243
2319
|
}
|
|
2244
2320
|
const fullResponse = chunks.join("");
|
|
2245
2321
|
const mockLog = {
|
|
@@ -2320,12 +2396,8 @@ async function testStreamingEndpoint(baseUrl, apiKey, path2, model, isOpenAI) {
|
|
|
2320
2396
|
};
|
|
2321
2397
|
}
|
|
2322
2398
|
} catch (err) {
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
error: String(err),
|
|
2326
|
-
latencyMs: Date.now() - startTime,
|
|
2327
|
-
streaming: true
|
|
2328
|
-
};
|
|
2399
|
+
clearTimeout(timeoutId);
|
|
2400
|
+
return createErrorResult(err, Date.now() - startTime, true);
|
|
2329
2401
|
}
|
|
2330
2402
|
}
|
|
2331
2403
|
function createTestLogEntry(providerName, path2, body, upstreamUrl, result, isTest) {
|
|
@@ -2346,7 +2418,7 @@ function createTestLogEntry(providerName, path2, body, upstreamUrl, result, isTe
|
|
|
2346
2418
|
userAgent: "provider-test",
|
|
2347
2419
|
origin: null,
|
|
2348
2420
|
upstreamUrl,
|
|
2349
|
-
error: result.success ? null : result.error,
|
|
2421
|
+
error: result.success ? null : result.error?.message ?? String(result.error),
|
|
2350
2422
|
isTest: true,
|
|
2351
2423
|
providerName
|
|
2352
2424
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$", "/api/config/paths"], "preloads": ["/assets/main-
|
|
1
|
+
const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/health", "/api/logs", "/api/models", "/api/providers", "/api/sessions", "/proxy/$", "/api/config/paths"], "preloads": ["/assets/main-CpIX1ZHy.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-Bf_WGooQ.js"] }, "/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/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/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/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" } }, "clientEntry": "/assets/main-CpIX1ZHy.js" });
|
|
2
2
|
export {
|
|
3
3
|
tsrStartManifest
|
|
4
4
|
};
|
package/.output/server/index.mjs
CHANGED
|
@@ -100,51 +100,51 @@ const assets = {
|
|
|
100
100
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
101
101
|
"type": "image/svg+xml",
|
|
102
102
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
103
|
-
"mtime": "2026-06-
|
|
103
|
+
"mtime": "2026-06-03T09:30:18.865Z",
|
|
104
104
|
"size": 5915,
|
|
105
105
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
106
106
|
},
|
|
107
107
|
"/assets/index-B3RwBPLW.css": {
|
|
108
108
|
"type": "text/css; charset=utf-8",
|
|
109
109
|
"etag": '"10c74-aXacU4DRFVsUwcC5jHnjoPRSlTA"',
|
|
110
|
-
"mtime": "2026-06-
|
|
110
|
+
"mtime": "2026-06-03T09:30:18.865Z",
|
|
111
111
|
"size": 68724,
|
|
112
112
|
"path": "../public/assets/index-B3RwBPLW.css"
|
|
113
113
|
},
|
|
114
114
|
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
115
115
|
"type": "image/jpeg",
|
|
116
116
|
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
117
|
-
"mtime": "2026-06-
|
|
117
|
+
"mtime": "2026-06-03T09:30:18.865Z",
|
|
118
118
|
"size": 6918,
|
|
119
119
|
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
120
120
|
},
|
|
121
121
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
122
122
|
"type": "image/svg+xml",
|
|
123
123
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
124
|
-
"mtime": "2026-06-
|
|
124
|
+
"mtime": "2026-06-03T09:30:18.862Z",
|
|
125
125
|
"size": 11256,
|
|
126
126
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
127
127
|
},
|
|
128
|
-
"/assets/main-
|
|
128
|
+
"/assets/main-CpIX1ZHy.js": {
|
|
129
129
|
"type": "text/javascript; charset=utf-8",
|
|
130
|
-
"etag": '"4db57-
|
|
131
|
-
"mtime": "2026-06-
|
|
130
|
+
"etag": '"4db57-PIyiLXQGvlFJuizUFXRhGOYXJwY"',
|
|
131
|
+
"mtime": "2026-06-03T09:30:18.865Z",
|
|
132
132
|
"size": 318295,
|
|
133
|
-
"path": "../public/assets/main-
|
|
133
|
+
"path": "../public/assets/main-CpIX1ZHy.js"
|
|
134
134
|
},
|
|
135
135
|
"/assets/qwen-CONDcHqt.png": {
|
|
136
136
|
"type": "image/png",
|
|
137
137
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
138
|
-
"mtime": "2026-06-
|
|
138
|
+
"mtime": "2026-06-03T09:30:18.865Z",
|
|
139
139
|
"size": 357059,
|
|
140
140
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
141
141
|
},
|
|
142
|
-
"/assets/index-
|
|
142
|
+
"/assets/index-Bf_WGooQ.js": {
|
|
143
143
|
"type": "text/javascript; charset=utf-8",
|
|
144
|
-
"etag": '"
|
|
145
|
-
"mtime": "2026-06-
|
|
146
|
-
"size":
|
|
147
|
-
"path": "../public/assets/index-
|
|
144
|
+
"etag": '"831df-gmdpd1CCnM4IdaxHIs9uyMgWFaY"',
|
|
145
|
+
"mtime": "2026-06-03T09:30:18.865Z",
|
|
146
|
+
"size": 537055,
|
|
147
|
+
"path": "../public/assets/index-Bf_WGooQ.js"
|
|
148
148
|
}
|
|
149
149
|
};
|
|
150
150
|
function readAsset(id) {
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { spawn, execSync } from "node:child_process";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
5
6
|
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = dirname(__filename);
|
|
@@ -35,8 +36,78 @@ for (let i = 0; i < args.length; i++) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Check if a port is in use and kill the process using it
|
|
41
|
+
*/
|
|
42
|
+
function killProcessOnPort(targetPort: number): void {
|
|
43
|
+
const platform = process.platform;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
let pids: number[] = [];
|
|
47
|
+
|
|
48
|
+
if (platform === "win32") {
|
|
49
|
+
// Windows: use netstat to find PID, then taskkill
|
|
50
|
+
const output = execSync(`netstat -ano | findstr :${targetPort}`, {
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
});
|
|
54
|
+
const lines = output.trim().split("\n");
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const parts = line.trim().split(/\s+/);
|
|
57
|
+
if (parts.length >= 5) {
|
|
58
|
+
const localAddress = parts[1] ?? "";
|
|
59
|
+
const pidStr = parts[4] ?? "";
|
|
60
|
+
if (localAddress !== "" && pidStr !== "" && localAddress.includes(`:${targetPort}`)) {
|
|
61
|
+
const pid = parseInt(pidStr, 10);
|
|
62
|
+
if (!isNaN(pid) && pid > 0) {
|
|
63
|
+
pids.push(pid);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Remove duplicates
|
|
69
|
+
pids = [...new Set(pids)];
|
|
70
|
+
|
|
71
|
+
for (const pid of pids) {
|
|
72
|
+
try {
|
|
73
|
+
console.log(`Killing process ${pid} on port ${port}...`);
|
|
74
|
+
execSync(`taskkill /PID ${pid} /F`, { encoding: "utf8", timeout: 5000 });
|
|
75
|
+
} catch {
|
|
76
|
+
// Process may have already exited
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
// Unix-like: use lsof
|
|
81
|
+
const output = execSync(`lsof -ti:${targetPort}`, { encoding: "utf8", timeout: 5000 });
|
|
82
|
+
const lines = output.trim().split("\n");
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const pid = parseInt(line.trim(), 10);
|
|
85
|
+
if (!isNaN(pid) && pid > 0) {
|
|
86
|
+
pids.push(pid);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Remove duplicates
|
|
90
|
+
pids = [...new Set(pids)];
|
|
91
|
+
|
|
92
|
+
for (const pid of pids) {
|
|
93
|
+
try {
|
|
94
|
+
console.log(`Killing process ${pid} on port ${targetPort}...`);
|
|
95
|
+
execSync(`kill -9 ${pid}`, { encoding: "utf8", timeout: 5000 });
|
|
96
|
+
} catch {
|
|
97
|
+
// Process may have already exited
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// No process found on port, which is fine
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
38
106
|
process.env["PORT"] = String(port);
|
|
39
107
|
|
|
108
|
+
// Kill any existing process on the port
|
|
109
|
+
killProcessOnPort(port);
|
|
110
|
+
|
|
40
111
|
const url = `http://localhost:${port}`;
|
|
41
112
|
|
|
42
113
|
console.log(`Server running at ${url}`);
|
|
@@ -10,6 +10,14 @@ import {
|
|
|
10
10
|
XCircle,
|
|
11
11
|
Minus,
|
|
12
12
|
ExternalLink,
|
|
13
|
+
AlertCircle,
|
|
14
|
+
Wifi,
|
|
15
|
+
WifiOff,
|
|
16
|
+
Lock,
|
|
17
|
+
Gauge,
|
|
18
|
+
Server,
|
|
19
|
+
HelpCircle,
|
|
20
|
+
Clock,
|
|
13
21
|
} from "lucide-react";
|
|
14
22
|
import type { ProviderConfig } from "../../proxy/providers";
|
|
15
23
|
|
|
@@ -18,9 +26,26 @@ const KNOWN_PROVIDER_DOCS: Record<string, string> = {
|
|
|
18
26
|
deepseek: "https://api-docs.deepseek.com/zh-cn/",
|
|
19
27
|
};
|
|
20
28
|
|
|
29
|
+
type ErrorType =
|
|
30
|
+
| "timeout"
|
|
31
|
+
| "network_unreachable"
|
|
32
|
+
| "connection_refused"
|
|
33
|
+
| "auth_failed"
|
|
34
|
+
| "rate_limited"
|
|
35
|
+
| "server_error"
|
|
36
|
+
| "invalid_response"
|
|
37
|
+
| "unknown";
|
|
38
|
+
|
|
39
|
+
type EnhancedError = {
|
|
40
|
+
message: string;
|
|
41
|
+
type: ErrorType;
|
|
42
|
+
details?: string;
|
|
43
|
+
hint?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
21
46
|
type TestResult = {
|
|
22
47
|
success: boolean;
|
|
23
|
-
error?:
|
|
48
|
+
error?: EnhancedError;
|
|
24
49
|
};
|
|
25
50
|
|
|
26
51
|
type NotConfigured = { notConfigured: true };
|
|
@@ -53,7 +78,37 @@ function hasSuccessField(result: TestResult | NotConfigured): result is TestResu
|
|
|
53
78
|
return Object.prototype.hasOwnProperty.call(result, "success");
|
|
54
79
|
}
|
|
55
80
|
|
|
56
|
-
function
|
|
81
|
+
function getErrorIcon(type: ErrorType): JSX.Element {
|
|
82
|
+
const iconProps = { className: "size-3", strokeWidth: 2 };
|
|
83
|
+
switch (type) {
|
|
84
|
+
case "timeout":
|
|
85
|
+
return <Clock {...iconProps} />;
|
|
86
|
+
case "network_unreachable":
|
|
87
|
+
return <WifiOff {...iconProps} />;
|
|
88
|
+
case "connection_refused":
|
|
89
|
+
return <Wifi {...iconProps} />;
|
|
90
|
+
case "auth_failed":
|
|
91
|
+
return <Lock {...iconProps} />;
|
|
92
|
+
case "rate_limited":
|
|
93
|
+
return <Gauge {...iconProps} />;
|
|
94
|
+
case "server_error":
|
|
95
|
+
return <Server {...iconProps} />;
|
|
96
|
+
case "invalid_response":
|
|
97
|
+
return <HelpCircle {...iconProps} />;
|
|
98
|
+
case "unknown":
|
|
99
|
+
return <AlertCircle {...iconProps} />;
|
|
100
|
+
default:
|
|
101
|
+
return <AlertCircle {...iconProps} />;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function TestStatus({
|
|
106
|
+
result,
|
|
107
|
+
isTesting,
|
|
108
|
+
}: {
|
|
109
|
+
result: TestResult | NotConfigured;
|
|
110
|
+
isTesting?: boolean;
|
|
111
|
+
}): JSX.Element {
|
|
57
112
|
if (!hasSuccessField(result)) {
|
|
58
113
|
return (
|
|
59
114
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
@@ -70,10 +125,26 @@ function TestStatus({ result }: { result: TestResult | NotConfigured }): JSX.Ele
|
|
|
70
125
|
</div>
|
|
71
126
|
);
|
|
72
127
|
}
|
|
128
|
+
|
|
129
|
+
const error = result.error;
|
|
130
|
+
const errorMessage = error?.message ?? "Connection failed";
|
|
131
|
+
const errorHint = error?.hint;
|
|
132
|
+
const errorType = error?.type ?? "unknown";
|
|
133
|
+
|
|
73
134
|
return (
|
|
74
|
-
<div className="flex
|
|
75
|
-
<
|
|
76
|
-
|
|
135
|
+
<div className="flex flex-col gap-1 min-w-0">
|
|
136
|
+
<div
|
|
137
|
+
className="flex items-center gap-1 text-xs text-red-600 min-w-0"
|
|
138
|
+
title={error?.details ?? errorMessage}
|
|
139
|
+
>
|
|
140
|
+
{getErrorIcon(errorType)}
|
|
141
|
+
<span className="truncate">{errorMessage}</span>
|
|
142
|
+
</div>
|
|
143
|
+
{errorHint !== undefined && (
|
|
144
|
+
<div className="text-xs text-muted-foreground pl-4 truncate" title={errorHint}>
|
|
145
|
+
{errorHint}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
77
148
|
</div>
|
|
78
149
|
);
|
|
79
150
|
}
|
|
@@ -135,7 +206,9 @@ export function ProviderCard({
|
|
|
135
206
|
<span className="font-medium">Anthropic:</span>{" "}
|
|
136
207
|
<span className="truncate">{provider.anthropicBaseUrl}</span>
|
|
137
208
|
</div>
|
|
138
|
-
{testResults &&
|
|
209
|
+
{testResults && (
|
|
210
|
+
<TestStatus result={testResults.anthropic.nonStreaming} isTesting={isTesting} />
|
|
211
|
+
)}
|
|
139
212
|
</div>
|
|
140
213
|
)}
|
|
141
214
|
|
|
@@ -145,7 +218,9 @@ export function ProviderCard({
|
|
|
145
218
|
<span className="font-medium">OpenAI:</span>{" "}
|
|
146
219
|
<span className="truncate">{provider.openaiBaseUrl}</span>
|
|
147
220
|
</div>
|
|
148
|
-
{testResults &&
|
|
221
|
+
{testResults && (
|
|
222
|
+
<TestStatus result={testResults.openai.nonStreaming} isTesting={isTesting} />
|
|
223
|
+
)}
|
|
149
224
|
</div>
|
|
150
225
|
)}
|
|
151
226
|
|
|
@@ -9,9 +9,24 @@ type ConfigPathsResponse = {
|
|
|
9
9
|
providerConfig: string;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
type EnhancedError = {
|
|
13
|
+
message: string;
|
|
14
|
+
type:
|
|
15
|
+
| "timeout"
|
|
16
|
+
| "network_unreachable"
|
|
17
|
+
| "connection_refused"
|
|
18
|
+
| "auth_failed"
|
|
19
|
+
| "rate_limited"
|
|
20
|
+
| "server_error"
|
|
21
|
+
| "invalid_response"
|
|
22
|
+
| "unknown";
|
|
23
|
+
details?: string;
|
|
24
|
+
hint?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
12
27
|
type TestResult = {
|
|
13
28
|
success: boolean;
|
|
14
|
-
error?:
|
|
29
|
+
error?: EnhancedError;
|
|
15
30
|
};
|
|
16
31
|
|
|
17
32
|
type NotConfigured = { notConfigured: true };
|
|
@@ -88,14 +103,23 @@ export function ProvidersPanel(): JSX.Element {
|
|
|
88
103
|
name: string;
|
|
89
104
|
apiKey: string;
|
|
90
105
|
model?: string;
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
format: "anthropic" | "openai";
|
|
107
|
+
baseUrl?: string;
|
|
93
108
|
}): void {
|
|
94
109
|
void (async () => {
|
|
110
|
+
// Convert baseUrl to format-specific URL
|
|
111
|
+
const payload = {
|
|
112
|
+
name: data.name,
|
|
113
|
+
apiKey: data.apiKey,
|
|
114
|
+
model: data.model,
|
|
115
|
+
format: data.format,
|
|
116
|
+
anthropicBaseUrl: data.format === "anthropic" ? data.baseUrl : undefined,
|
|
117
|
+
openaiBaseUrl: data.format === "openai" ? data.baseUrl : undefined,
|
|
118
|
+
};
|
|
95
119
|
const res = await fetch("/api/providers", {
|
|
96
120
|
method: "POST",
|
|
97
121
|
headers: { "Content-Type": "application/json" },
|
|
98
|
-
body: JSON.stringify(
|
|
122
|
+
body: JSON.stringify(payload),
|
|
99
123
|
});
|
|
100
124
|
if (!res.ok) {
|
|
101
125
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
@@ -116,15 +140,24 @@ export function ProvidersPanel(): JSX.Element {
|
|
|
116
140
|
name: string;
|
|
117
141
|
apiKey: string;
|
|
118
142
|
model?: string;
|
|
119
|
-
|
|
120
|
-
|
|
143
|
+
format: "anthropic" | "openai";
|
|
144
|
+
baseUrl?: string;
|
|
121
145
|
}): void {
|
|
122
146
|
if (!editingProvider) return;
|
|
123
147
|
void (async () => {
|
|
148
|
+
// Convert baseUrl to format-specific URL
|
|
149
|
+
const payload = {
|
|
150
|
+
name: data.name,
|
|
151
|
+
apiKey: data.apiKey,
|
|
152
|
+
model: data.model,
|
|
153
|
+
format: data.format,
|
|
154
|
+
anthropicBaseUrl: data.format === "anthropic" ? data.baseUrl : undefined,
|
|
155
|
+
openaiBaseUrl: data.format === "openai" ? data.baseUrl : undefined,
|
|
156
|
+
};
|
|
124
157
|
const res = await fetch(`/api/providers/${editingProvider.id}`, {
|
|
125
158
|
method: "PUT",
|
|
126
159
|
headers: { "Content-Type": "application/json" },
|
|
127
|
-
body: JSON.stringify(
|
|
160
|
+
body: JSON.stringify(payload),
|
|
128
161
|
});
|
|
129
162
|
if (!res.ok) {
|
|
130
163
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|