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.
Files changed (67) hide show
  1. package/config/recommended-providers.json +374 -379
  2. package/dist/admin/quick-setup.js +158 -1
  3. package/dist/admin/settings-import-export.js +37 -0
  4. package/dist/config/model-context.js +9 -2
  5. package/dist/config/recommended-providers.json +374 -379
  6. package/dist/config/recommended.d.ts +6 -2
  7. package/dist/proxy/proxy-core.d.ts +8 -3
  8. package/dist/proxy/proxy-core.js +54 -3
  9. package/frontend-dist/assets/{AuthLayout-CuVriBqD.js → AuthLayout-dvFLorc3.js} +1 -1
  10. package/frontend-dist/assets/{Card-DTwMrsYP.js → Card-CogrcVqG.js} +1 -1
  11. package/frontend-dist/assets/{CardContent-CMzFQ6-k.js → CardContent-DITNPZ_X.js} +1 -1
  12. package/frontend-dist/assets/{CardTitle-Be8-p4BH.js → CardTitle-gBp1g2gI.js} +1 -1
  13. package/frontend-dist/assets/{CascadingModelSelect-Cc_D72Jk.js → CascadingModelSelect-Cl3SPCFU.js} +1 -1
  14. package/frontend-dist/assets/{Checkbox-DPmC2AGz.js → Checkbox-D7h-RQpQ.js} +1 -1
  15. package/frontend-dist/assets/{CollapsibleContent-CU4g21xb.js → CollapsibleContent-CzuuEMvx.js} +1 -1
  16. package/frontend-dist/assets/{CollapsibleTrigger-nWHrZjCc.js → CollapsibleTrigger-Boopy6Xr.js} +1 -1
  17. package/frontend-dist/assets/{ConcurrencyControl-bxN52Lwk.js → ConcurrencyControl-BUJN3r0I.js} +1 -1
  18. package/frontend-dist/assets/{Dashboard-DdW4nMfn.js → Dashboard-DB9wWUNq.js} +1 -1
  19. package/frontend-dist/assets/{Input-S-dhEtyh.js → Input-BvkV_UYD.js} +1 -1
  20. package/frontend-dist/assets/{Label-CoxfPXiC.js → Label-DPRjVppM.js} +1 -1
  21. package/frontend-dist/assets/{Login-BWIc4K_R.js → Login-BBGLgROU.js} +1 -1
  22. package/frontend-dist/assets/{Logs-DcYLAi1u.js → Logs-BnVGZ_9e.js} +1 -1
  23. package/frontend-dist/assets/{ModelMappings-CRlcpCbC.js → ModelMappings-DW1YYtv4.js} +1 -1
  24. package/frontend-dist/assets/{Monitor-kQgtcdgb.js → Monitor-bOkYaTAo.js} +1 -1
  25. package/frontend-dist/assets/{Providers-DhQorqVw.js → Providers-DXzfD-vf.js} +1 -1
  26. package/frontend-dist/assets/{ProxyEnhancement-DmDjBAVY.js → ProxyEnhancement-CZl10ofA.js} +1 -1
  27. package/frontend-dist/assets/QuickSetup-1SxZvUaD.js +1 -0
  28. package/frontend-dist/assets/{RetryRules-BHHOV_nS.js → RetryRules-k47fvN7j.js} +1 -1
  29. package/frontend-dist/assets/{RouterKeys-DGPVdweT.js → RouterKeys-BSu4mZX1.js} +1 -1
  30. package/frontend-dist/assets/{RovingFocusItem-S9iMSjYu.js → RovingFocusItem-DQch7uJj.js} +1 -1
  31. package/frontend-dist/assets/{Schedules-BiXQOfZ6.js → Schedules-DJZmLT3p.js} +1 -1
  32. package/frontend-dist/assets/{Settings-BaGG5H3_.js → Settings-Dsl_wN0I.js} +1 -1
  33. package/frontend-dist/assets/{Setup-Clo5FbAq.js → Setup-CpOmnV2J.js} +1 -1
  34. package/frontend-dist/assets/{Skeleton-LpA7ym3h.js → Skeleton-pR_m-iRh.js} +1 -1
  35. package/frontend-dist/assets/{Switch-BuzP1BOq.js → Switch-Bjz2-rRD.js} +1 -1
  36. package/frontend-dist/assets/{TableHeader-Ch_FQSsR.js → TableHeader-DbI4FrzV.js} +1 -1
  37. package/frontend-dist/assets/{TabsTrigger-CkXSdTsW.js → TabsTrigger-DaSU-LWF.js} +1 -1
  38. package/frontend-dist/assets/{TooltipTrigger-BvvJIVjq.js → TooltipTrigger-BlpAhWt7.js} +1 -1
  39. package/frontend-dist/assets/{UnifiedRequestDialog-D4Etn-XR.js → UnifiedRequestDialog--Zu0ozM9.js} +1 -1
  40. package/frontend-dist/assets/{VisuallyHiddenInput-BKf1XhBt.js → VisuallyHiddenInput-DyBqOt9m.js} +1 -1
  41. package/frontend-dist/assets/arrow-down-BsjAVIky.js +1 -0
  42. package/frontend-dist/assets/{badge-C2q8_BSL.js → badge-BYqBmHXm.js} +1 -1
  43. package/frontend-dist/assets/{button-Dfujvdrh.js → button-D7-mGBSV.js} +2 -2
  44. package/frontend-dist/assets/chevron-right-B-FGEL1C.js +1 -0
  45. package/frontend-dist/assets/{dialog-L4DtmRL0.js → dialog-FlJt6TDi.js} +1 -1
  46. package/frontend-dist/assets/{image-CxxlX0UA.js → image-Cz-q1ZxQ.js} +1 -1
  47. package/frontend-dist/assets/{index-C-hnyfjw.js → index-ciH8oXVi.js} +2 -2
  48. package/frontend-dist/assets/model-patches-jojmG1hL.js +1 -0
  49. package/frontend-dist/assets/{pencil-DBujtKN9.js → pencil-D5z5BLFS.js} +1 -1
  50. package/frontend-dist/assets/plus-DM82m_ph.js +1 -0
  51. package/frontend-dist/assets/{quickSetup-CjxI1qgV.js → quickSetup-DUBCRvkP.js} +1 -1
  52. package/frontend-dist/assets/{quickSetup-zSIVom2e.js → quickSetup-DpoJ3AU-.js} +1 -1
  53. package/frontend-dist/assets/search-CVf7YI47.js +1 -0
  54. package/frontend-dist/assets/{sparkles-DZokYUQQ.js → sparkles-DxcHbioB.js} +1 -1
  55. package/frontend-dist/assets/{transform-domain-GHReVoGj.js → transform-domain-0bvsS4wY.js} +1 -1
  56. package/frontend-dist/assets/{trash-2-V6kA11Le.js → trash-2-DHcKpmZl.js} +1 -1
  57. package/frontend-dist/assets/{useClipboard-bwRknJ9i.js → useClipboard-D3nudXL0.js} +1 -1
  58. package/frontend-dist/assets/{useLogRetention-B9_3BnSj.js → useLogRetention-CUpxQilv.js} +1 -1
  59. package/frontend-dist/assets/{useProviderGroups-DogUym32.js → useProviderGroups-C9xl3B2L.js} +1 -1
  60. package/frontend-dist/index.html +2 -2
  61. package/package.json +1 -1
  62. package/frontend-dist/assets/QuickSetup-Bbk6Bqf0.js +0 -1
  63. package/frontend-dist/assets/arrow-down-DwqIrYgN.js +0 -1
  64. package/frontend-dist/assets/chevron-right-CePT5OcU.js +0 -1
  65. package/frontend-dist/assets/model-patches-cjz0IqAT.js +0 -1
  66. package/frontend-dist/assets/plus-8BGkjkCq.js +0 -1
  67. 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 ── 只有 omni 版本支持图片,pro 版本是纯文本
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.5": ["text", "image", "audio", "video"],
142
+ "mimo-v2-pro": ["text"],
136
143
  };
137
144
  export const DEFAULT_CONTEXT_WINDOW = 200000;
138
145
  export const OVERFLOW_THRESHOLD = 1000000;