llm-simple-router 1.0.4 → 1.0.6
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/config/recommended-providers.json +374 -379
- package/dist/admin/quick-setup.js +158 -1
- package/dist/admin/settings-import-export.js +37 -0
- package/dist/config/model-context.js +9 -2
- package/dist/config/recommended-providers.json +374 -379
- package/dist/config/recommended.d.ts +6 -2
- package/dist/proxy/proxy-core.d.ts +8 -3
- package/dist/proxy/proxy-core.js +54 -3
- package/frontend-dist/assets/{AuthLayout-CuVriBqD.js → AuthLayout-dvFLorc3.js} +1 -1
- package/frontend-dist/assets/{Card-DTwMrsYP.js → Card-CogrcVqG.js} +1 -1
- package/frontend-dist/assets/{CardContent-CMzFQ6-k.js → CardContent-DITNPZ_X.js} +1 -1
- package/frontend-dist/assets/{CardTitle-Be8-p4BH.js → CardTitle-gBp1g2gI.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-Cc_D72Jk.js → CascadingModelSelect-Cl3SPCFU.js} +1 -1
- package/frontend-dist/assets/{Checkbox-DPmC2AGz.js → Checkbox-D7h-RQpQ.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-CU4g21xb.js → CollapsibleContent-CzuuEMvx.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-nWHrZjCc.js → CollapsibleTrigger-Boopy6Xr.js} +1 -1
- package/frontend-dist/assets/{ConcurrencyControl-bxN52Lwk.js → ConcurrencyControl-BUJN3r0I.js} +1 -1
- package/frontend-dist/assets/{Dashboard-DdW4nMfn.js → Dashboard-DB9wWUNq.js} +1 -1
- package/frontend-dist/assets/{Input-S-dhEtyh.js → Input-BvkV_UYD.js} +1 -1
- package/frontend-dist/assets/{Label-CoxfPXiC.js → Label-DPRjVppM.js} +1 -1
- package/frontend-dist/assets/{Login-BWIc4K_R.js → Login-BBGLgROU.js} +1 -1
- package/frontend-dist/assets/{Logs-DcYLAi1u.js → Logs-BnVGZ_9e.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-CRlcpCbC.js → ModelMappings-DW1YYtv4.js} +1 -1
- package/frontend-dist/assets/{Monitor-kQgtcdgb.js → Monitor-bOkYaTAo.js} +1 -1
- package/frontend-dist/assets/{Providers-DhQorqVw.js → Providers-DXzfD-vf.js} +1 -1
- package/frontend-dist/assets/{ProxyEnhancement-DmDjBAVY.js → ProxyEnhancement-CZl10ofA.js} +1 -1
- package/frontend-dist/assets/QuickSetup-1SxZvUaD.js +1 -0
- package/frontend-dist/assets/{RetryRules-BHHOV_nS.js → RetryRules-k47fvN7j.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-DGPVdweT.js → RouterKeys-BSu4mZX1.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-S9iMSjYu.js → RovingFocusItem-DQch7uJj.js} +1 -1
- package/frontend-dist/assets/{Schedules-BiXQOfZ6.js → Schedules-DJZmLT3p.js} +1 -1
- package/frontend-dist/assets/{Settings-BaGG5H3_.js → Settings-Dsl_wN0I.js} +1 -1
- package/frontend-dist/assets/{Setup-Clo5FbAq.js → Setup-CpOmnV2J.js} +1 -1
- package/frontend-dist/assets/{Skeleton-LpA7ym3h.js → Skeleton-pR_m-iRh.js} +1 -1
- package/frontend-dist/assets/{Switch-BuzP1BOq.js → Switch-Bjz2-rRD.js} +1 -1
- package/frontend-dist/assets/{TableHeader-Ch_FQSsR.js → TableHeader-DbI4FrzV.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-CkXSdTsW.js → TabsTrigger-DaSU-LWF.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-BvvJIVjq.js → TooltipTrigger-BlpAhWt7.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-D4Etn-XR.js → UnifiedRequestDialog--Zu0ozM9.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-BKf1XhBt.js → VisuallyHiddenInput-DyBqOt9m.js} +1 -1
- package/frontend-dist/assets/arrow-down-BsjAVIky.js +1 -0
- package/frontend-dist/assets/{badge-C2q8_BSL.js → badge-BYqBmHXm.js} +1 -1
- package/frontend-dist/assets/{button-Dfujvdrh.js → button-D7-mGBSV.js} +2 -2
- package/frontend-dist/assets/chevron-right-B-FGEL1C.js +1 -0
- package/frontend-dist/assets/{dialog-L4DtmRL0.js → dialog-FlJt6TDi.js} +1 -1
- package/frontend-dist/assets/{image-CxxlX0UA.js → image-Cz-q1ZxQ.js} +1 -1
- package/frontend-dist/assets/{index-C-hnyfjw.js → index-ciH8oXVi.js} +2 -2
- package/frontend-dist/assets/model-patches-jojmG1hL.js +1 -0
- package/frontend-dist/assets/{pencil-DBujtKN9.js → pencil-D5z5BLFS.js} +1 -1
- package/frontend-dist/assets/plus-DM82m_ph.js +1 -0
- package/frontend-dist/assets/{quickSetup-CjxI1qgV.js → quickSetup-DUBCRvkP.js} +1 -1
- package/frontend-dist/assets/{quickSetup-zSIVom2e.js → quickSetup-DpoJ3AU-.js} +1 -1
- package/frontend-dist/assets/search-CVf7YI47.js +1 -0
- package/frontend-dist/assets/{sparkles-DZokYUQQ.js → sparkles-DxcHbioB.js} +1 -1
- package/frontend-dist/assets/{transform-domain-GHReVoGj.js → transform-domain-0bvsS4wY.js} +1 -1
- package/frontend-dist/assets/{trash-2-V6kA11Le.js → trash-2-DHcKpmZl.js} +1 -1
- package/frontend-dist/assets/{useClipboard-bwRknJ9i.js → useClipboard-D3nudXL0.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-B9_3BnSj.js → useLogRetention-CUpxQilv.js} +1 -1
- package/frontend-dist/assets/{useProviderGroups-DogUym32.js → useProviderGroups-C9xl3B2L.js} +1 -1
- package/frontend-dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend-dist/assets/QuickSetup-Bbk6Bqf0.js +0 -1
- package/frontend-dist/assets/arrow-down-DwqIrYgN.js +0 -1
- package/frontend-dist/assets/chevron-right-CePT5OcU.js +0 -1
- package/frontend-dist/assets/model-patches-cjz0IqAT.js +0 -1
- package/frontend-dist/assets/plus-8BGkjkCq.js +0 -1
- package/frontend-dist/assets/search-BqSw1jeO.js +0 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { request as httpRequest } from "http";
|
|
3
|
+
import { request as httpsRequest } from "https";
|
|
2
4
|
import { createProvider, PROVIDER_CONCURRENCY_DEFAULTS } from "../db/providers.js";
|
|
3
5
|
import { createMappingGroup, updateMappingGroup } from "../db/mappings.js";
|
|
4
6
|
import { createRetryRule } from "../db/retry-rules.js";
|
|
5
7
|
import { upsertTransformRule } from "../db/transform-rules.js";
|
|
6
8
|
import { encrypt } from "../utils/crypto.js";
|
|
7
9
|
import { getSetting } from "../db/settings.js";
|
|
8
|
-
import { HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_CONFLICT } from "./constants.js";
|
|
10
|
+
import { HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_BAD_GATEWAY, HTTP_CONFLICT } from "./constants.js";
|
|
9
11
|
import { API_CODE, apiError } from "./api-response.js";
|
|
10
12
|
const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
11
13
|
const API_KEY_PREVIEW_MIN_LENGTH = 8;
|
|
@@ -222,5 +224,160 @@ export const adminQuickSetupRoutes = (app, options, done) => {
|
|
|
222
224
|
});
|
|
223
225
|
return reply.code(HTTP_CREATED).send({ success: true, provider_id: providerId });
|
|
224
226
|
});
|
|
227
|
+
// ---------- Test Connection ----------
|
|
228
|
+
const TestConnectionSchema = Type.Object({
|
|
229
|
+
api_type: Type.Union([Type.Literal("openai"), Type.Literal("openai-responses"), Type.Literal("anthropic")]),
|
|
230
|
+
base_url: Type.String({ minLength: 1 }),
|
|
231
|
+
upstream_path: Type.Optional(Type.Union([Type.String({ minLength: 1 }), Type.Null()])),
|
|
232
|
+
api_key: Type.String({ minLength: 1 }),
|
|
233
|
+
model: Type.Optional(Type.String()),
|
|
234
|
+
});
|
|
235
|
+
app.post("/admin/api/test-connection", { schema: { body: TestConnectionSchema } }, async (request, reply) => {
|
|
236
|
+
const body = request.body;
|
|
237
|
+
const apiType = body.api_type;
|
|
238
|
+
const baseUrl = body.base_url.replace(/\/+$/, "");
|
|
239
|
+
const upstreamPath = body.upstream_path ?? null;
|
|
240
|
+
const apiKey = body.api_key;
|
|
241
|
+
// Determine model and path based on api_type
|
|
242
|
+
let targetPath;
|
|
243
|
+
let reqBody;
|
|
244
|
+
let authHeader;
|
|
245
|
+
if (apiType === "anthropic") {
|
|
246
|
+
targetPath = upstreamPath ?? "/v1/messages";
|
|
247
|
+
reqBody = {
|
|
248
|
+
model: body.model ?? "claude-3-5-haiku-20241022",
|
|
249
|
+
max_tokens: 32,
|
|
250
|
+
messages: [{ role: "user", content: "Hi" }],
|
|
251
|
+
};
|
|
252
|
+
authHeader = `x-api-key`;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// openai or openai-responses
|
|
256
|
+
targetPath = upstreamPath ?? "/v1/chat/completions";
|
|
257
|
+
reqBody = {
|
|
258
|
+
model: body.model ?? "gpt-4o-mini",
|
|
259
|
+
max_tokens: 32,
|
|
260
|
+
messages: [
|
|
261
|
+
{ role: "system", content: "You are a helpful assistant." },
|
|
262
|
+
{ role: "user", content: "Say hi in one word." },
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
authHeader = "authorization";
|
|
266
|
+
}
|
|
267
|
+
// Build full URL with dedup logic
|
|
268
|
+
const fullUrl = buildTestUrl(baseUrl, targetPath);
|
|
269
|
+
try {
|
|
270
|
+
const result = await sendTestRequest(fullUrl, apiType, apiKey, authHeader, reqBody);
|
|
271
|
+
return reply.send({ ok: true, model: body.model, latency_ms: result.latencyMs });
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
const message = err instanceof Error ? err.message : JSON.stringify(err);
|
|
275
|
+
return reply.code(HTTP_BAD_GATEWAY).send({ ok: false, error: message });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
225
278
|
done();
|
|
226
279
|
};
|
|
280
|
+
// ---------- Test Connection Helpers ----------
|
|
281
|
+
const TEST_REQUEST_TIMEOUT_MS = 15_000;
|
|
282
|
+
const HTTPS_DEFAULT_PORT = 443;
|
|
283
|
+
const HTTP_DEFAULT_PORT = 80;
|
|
284
|
+
const HTTP_SUCCESS_MIN = 200;
|
|
285
|
+
const HTTP_SUCCESS_MAX = 300;
|
|
286
|
+
/** Build full URL, applying dedup logic for base_url already containing the path */
|
|
287
|
+
function buildTestUrl(baseUrl, upstreamPath) {
|
|
288
|
+
const KNOWN_SUFFIXES = ["/chat/completions", "/messages", "/responses"];
|
|
289
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
290
|
+
if (normalized.endsWith(upstreamPath))
|
|
291
|
+
return normalized;
|
|
292
|
+
for (const suffix of KNOWN_SUFFIXES) {
|
|
293
|
+
if (normalized.endsWith(suffix))
|
|
294
|
+
return normalized;
|
|
295
|
+
}
|
|
296
|
+
// Check for /v1 prefix overlap
|
|
297
|
+
const versionMatch = upstreamPath.match(/^(\/v\d+)(.*)/);
|
|
298
|
+
if (versionMatch) {
|
|
299
|
+
const [, prefix, rest] = versionMatch;
|
|
300
|
+
if (normalized.endsWith(prefix))
|
|
301
|
+
return `${normalized}${rest}`;
|
|
302
|
+
}
|
|
303
|
+
// Generic overlap detection
|
|
304
|
+
const segments = upstreamPath.split("/");
|
|
305
|
+
const MIN_OVERLAP_SEGMENTS = 2;
|
|
306
|
+
for (let len = segments.length - 1; len >= MIN_OVERLAP_SEGMENTS; len--) {
|
|
307
|
+
const candidate = segments.slice(0, len).join("/");
|
|
308
|
+
if (candidate.length > 0 && normalized.endsWith(candidate)) {
|
|
309
|
+
return `${normalized}${upstreamPath.slice(candidate.length)}`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!upstreamPath.startsWith("/"))
|
|
313
|
+
return `${normalized}/${upstreamPath}`;
|
|
314
|
+
return `${normalized}${upstreamPath}`;
|
|
315
|
+
}
|
|
316
|
+
/** Send a real test request to the upstream provider */
|
|
317
|
+
function sendTestRequest(fullUrl, apiType, apiKey, authHeaderKey, reqBody) {
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
const start = Date.now();
|
|
320
|
+
const url = new URL(fullUrl);
|
|
321
|
+
const payload = JSON.stringify(reqBody);
|
|
322
|
+
const headers = {
|
|
323
|
+
"content-type": "application/json",
|
|
324
|
+
"accept": "application/json",
|
|
325
|
+
"user-agent": "llm-simple-router/test-connection",
|
|
326
|
+
};
|
|
327
|
+
if (authHeaderKey === "x-api-key") {
|
|
328
|
+
headers["x-api-key"] = apiKey;
|
|
329
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
330
|
+
headers["content-type"] = "application/json";
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
headers["authorization"] = `Bearer ${apiKey}`;
|
|
334
|
+
}
|
|
335
|
+
const options = {
|
|
336
|
+
hostname: url.hostname,
|
|
337
|
+
port: url.port || (url.protocol === "https:" ? HTTPS_DEFAULT_PORT : HTTP_DEFAULT_PORT),
|
|
338
|
+
path: url.pathname + url.search,
|
|
339
|
+
method: "POST",
|
|
340
|
+
headers: { ...headers, "content-length": `${Buffer.byteLength(payload)}` },
|
|
341
|
+
};
|
|
342
|
+
const mod = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
343
|
+
const req = mod(options, (res) => {
|
|
344
|
+
const chunks = [];
|
|
345
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
346
|
+
res.on("end", () => {
|
|
347
|
+
const statusCode = res.statusCode ?? 0;
|
|
348
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
349
|
+
const latencyMs = Date.now() - start;
|
|
350
|
+
if (statusCode >= HTTP_SUCCESS_MIN && statusCode < HTTP_SUCCESS_MAX) {
|
|
351
|
+
resolve({ latencyMs });
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
// Try to extract error message from response
|
|
355
|
+
let errMsg = `HTTP ${statusCode}`;
|
|
356
|
+
try {
|
|
357
|
+
const parsed = JSON.parse(body);
|
|
358
|
+
const error = parsed.error;
|
|
359
|
+
if (error?.message) {
|
|
360
|
+
errMsg = typeof error.message === "string" ? error.message : JSON.stringify(error.message);
|
|
361
|
+
}
|
|
362
|
+
else if (parsed.message) {
|
|
363
|
+
errMsg = typeof parsed.message === "string" ? parsed.message : JSON.stringify(parsed.message);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// response body is not valid JSON, keep default HTTP error
|
|
368
|
+
void 0;
|
|
369
|
+
}
|
|
370
|
+
reject(new Error(errMsg));
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
res.on("error", (err) => reject(err));
|
|
374
|
+
});
|
|
375
|
+
req.setTimeout(TEST_REQUEST_TIMEOUT_MS, () => {
|
|
376
|
+
req.destroy();
|
|
377
|
+
reject(new Error("Connection timed out"));
|
|
378
|
+
});
|
|
379
|
+
req.on("error", (err) => reject(err));
|
|
380
|
+
req.write(payload);
|
|
381
|
+
req.end();
|
|
382
|
+
});
|
|
383
|
+
}
|
|
@@ -36,6 +36,26 @@ export const adminImportExportRoutes = (app, options, done) => {
|
|
|
36
36
|
}
|
|
37
37
|
catch { /* eslint-disable-line taste/no-silent-catch -- 无法解密则保留原值 */ }
|
|
38
38
|
}
|
|
39
|
+
// 解密 endpoints JSON 内嵌套的 api_key
|
|
40
|
+
if (typeof row.endpoints === "string" && row.endpoints) {
|
|
41
|
+
try {
|
|
42
|
+
const eps = JSON.parse(row.endpoints);
|
|
43
|
+
for (const ep of eps) {
|
|
44
|
+
if (typeof ep.api_key === "string" && ep.api_key) {
|
|
45
|
+
ep.api_key = decrypt(ep.api_key, encryptionKey);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
row.endpoints = JSON.stringify(eps);
|
|
49
|
+
}
|
|
50
|
+
catch { /* eslint-disable-line taste/no-silent-catch -- 无法解密则保留原值 */ }
|
|
51
|
+
}
|
|
52
|
+
// 解密 proxy_password
|
|
53
|
+
if (typeof row.proxy_password === "string" && row.proxy_password) {
|
|
54
|
+
try {
|
|
55
|
+
row.proxy_password = decrypt(row.proxy_password, encryptionKey);
|
|
56
|
+
}
|
|
57
|
+
catch { /* eslint-disable-line taste/no-silent-catch -- 无法解密则保留原值 */ }
|
|
58
|
+
}
|
|
39
59
|
}
|
|
40
60
|
for (const row of (data.router_keys || [])) {
|
|
41
61
|
if (typeof row.key_encrypted === "string") {
|
|
@@ -74,6 +94,23 @@ export const adminImportExportRoutes = (app, options, done) => {
|
|
|
74
94
|
if (typeof row.api_key === "string" && row.api_key) {
|
|
75
95
|
row.api_key = encrypt(row.api_key, encryptionKey);
|
|
76
96
|
}
|
|
97
|
+
// 重加密 endpoints JSON 内嵌套的 api_key
|
|
98
|
+
if (typeof row.endpoints === "string" && row.endpoints) {
|
|
99
|
+
try {
|
|
100
|
+
const eps = JSON.parse(row.endpoints);
|
|
101
|
+
for (const ep of eps) {
|
|
102
|
+
if (typeof ep.api_key === "string" && ep.api_key) {
|
|
103
|
+
ep.api_key = encrypt(ep.api_key, encryptionKey);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
row.endpoints = JSON.stringify(eps);
|
|
107
|
+
}
|
|
108
|
+
catch { /* eslint-disable-line taste/no-silent-catch -- 无法解析则保留原值 */ }
|
|
109
|
+
}
|
|
110
|
+
// 重加密 proxy_password
|
|
111
|
+
if (typeof row.proxy_password === "string" && row.proxy_password) {
|
|
112
|
+
row.proxy_password = encrypt(row.proxy_password, encryptionKey);
|
|
113
|
+
}
|
|
77
114
|
}
|
|
78
115
|
for (const row of (importData.router_keys || [])) {
|
|
79
116
|
if (typeof row.key === "string") {
|
|
@@ -73,6 +73,11 @@ export const MODEL_CONTEXT_WINDOWS = {
|
|
|
73
73
|
"step-2-16k": 16000,
|
|
74
74
|
"step-1-8k": 8000,
|
|
75
75
|
"step-1-32k": 32000,
|
|
76
|
+
// 小米 MiMo
|
|
77
|
+
"mimo-v2.5-pro": 1000000,
|
|
78
|
+
"mimo-v2.5": 1000000,
|
|
79
|
+
"mimo-v2-omni": 128000,
|
|
80
|
+
"mimo-v2-pro": 128000,
|
|
76
81
|
// 硅基流动
|
|
77
82
|
"deepseek-ai/DeepSeek-V3.2-Exp": 128000,
|
|
78
83
|
"deepseek-ai/DeepSeek-R1": 128000,
|
|
@@ -130,9 +135,11 @@ export const MODEL_CAPABILITIES = {
|
|
|
130
135
|
"qwen3.5-flash": ["text", "image"],
|
|
131
136
|
// ── 火山引擎 ── Doubao Seed 2.0 Pro 规格:Input Text, Images, Video
|
|
132
137
|
"doubao-seed-2-0-pro-260215": ["text", "image", "video"],
|
|
133
|
-
// ── 小米 MiMo ──
|
|
138
|
+
// ── 小米 MiMo ──
|
|
139
|
+
"mimo-v2.5-pro": ["text", "image"],
|
|
140
|
+
"mimo-v2.5": ["text", "image"],
|
|
134
141
|
"mimo-v2-omni": ["text", "image", "audio", "video"],
|
|
135
|
-
"mimo-v2
|
|
142
|
+
"mimo-v2-pro": ["text"],
|
|
136
143
|
};
|
|
137
144
|
export const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
138
145
|
export const OVERFLOW_THRESHOLD = 1000000;
|