llm-simple-router 0.5.1 → 0.5.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.
Files changed (149) hide show
  1. package/config/recommended-providers.json +76 -0
  2. package/config/recommended-retry-rules.json +10 -0
  3. package/dist/admin/api-response.d.ts +27 -0
  4. package/dist/admin/api-response.js +40 -0
  5. package/dist/admin/constants.d.ts +0 -2
  6. package/dist/admin/constants.js +0 -3
  7. package/dist/admin/groups.js +9 -5
  8. package/dist/admin/logs.js +3 -2
  9. package/dist/admin/mappings.js +7 -6
  10. package/dist/admin/metrics.js +23 -5
  11. package/dist/admin/monitor.js +2 -1
  12. package/dist/admin/providers.js +13 -4
  13. package/dist/admin/proxy-enhancement.js +11 -6
  14. package/dist/admin/recommended.js +1 -9
  15. package/dist/admin/retry-rules.js +8 -4
  16. package/dist/admin/router-keys.js +5 -1
  17. package/dist/admin/settings-import-export.js +3 -2
  18. package/dist/admin/settings.js +7 -5
  19. package/dist/admin/setup.js +3 -2
  20. package/dist/admin/stats.js +20 -3
  21. package/dist/admin/upgrade.js +8 -7
  22. package/dist/admin/usage.js +12 -24
  23. package/dist/config.d.ts +1 -1
  24. package/dist/config.js +1 -1
  25. package/dist/constants.d.ts +3 -0
  26. package/dist/constants.js +11 -0
  27. package/dist/db/index.d.ts +3 -3
  28. package/dist/db/index.js +2 -2
  29. package/dist/db/mappings.js +5 -8
  30. package/dist/db/metrics.js +3 -4
  31. package/dist/db/providers.d.ts +8 -0
  32. package/dist/db/providers.js +6 -0
  33. package/dist/db/retry-rules.d.ts +1 -0
  34. package/dist/db/retry-rules.js +3 -0
  35. package/dist/db/stats.d.ts +1 -2
  36. package/dist/db/stats.js +7 -11
  37. package/dist/index.js +52 -34
  38. package/dist/metrics/metrics-extractor.js +1 -1
  39. package/dist/metrics/sse-parser.js +2 -0
  40. package/dist/middleware/admin-auth.js +6 -5
  41. package/dist/middleware/auth.js +1 -10
  42. package/dist/monitor/request-tracker.d.ts +1 -0
  43. package/dist/monitor/request-tracker.js +9 -45
  44. package/dist/monitor/runtime-collector.js +1 -1
  45. package/dist/monitor/stream-content-accumulator.d.ts +14 -0
  46. package/dist/monitor/stream-content-accumulator.js +58 -0
  47. package/dist/proxy/anthropic.d.ts +2 -1
  48. package/dist/proxy/anthropic.js +3 -3
  49. package/dist/proxy/enhancement/directive-parser.d.ts +18 -0
  50. package/dist/proxy/{directive-parser.js → enhancement/directive-parser.js} +44 -0
  51. package/dist/proxy/{enhancement-handler.js → enhancement/enhancement-handler.js} +152 -32
  52. package/dist/proxy/enhancement/index.d.ts +3 -0
  53. package/dist/proxy/enhancement/index.js +3 -0
  54. package/dist/proxy/{response-cleaner.js → enhancement/response-cleaner.js} +14 -0
  55. package/dist/proxy/log-helpers.d.ts +1 -1
  56. package/dist/proxy/mapping-resolver.js +4 -4
  57. package/dist/proxy/openai.d.ts +2 -1
  58. package/dist/proxy/openai.js +4 -4
  59. package/dist/proxy/orchestrator.d.ts +0 -1
  60. package/dist/proxy/orchestrator.js +1 -3
  61. package/dist/proxy/proxy-core.d.ts +0 -4
  62. package/dist/proxy/proxy-core.js +0 -2
  63. package/dist/proxy/proxy-handler.d.ts +1 -1
  64. package/dist/proxy/proxy-handler.js +52 -132
  65. package/dist/proxy/proxy-logging.d.ts +0 -2
  66. package/dist/proxy/proxy-logging.js +1 -3
  67. package/dist/proxy/resilience.d.ts +5 -2
  68. package/dist/proxy/resilience.js +16 -7
  69. package/dist/proxy/strategy/failover.js +2 -7
  70. package/dist/proxy/strategy/random.js +2 -2
  71. package/dist/proxy/strategy/round-robin.js +2 -2
  72. package/dist/proxy/strategy/scheduled.js +1 -8
  73. package/dist/proxy/strategy/targets-rule.d.ts +1 -0
  74. package/dist/proxy/strategy/targets-rule.js +5 -0
  75. package/dist/proxy/transport-fn.d.ts +25 -0
  76. package/dist/proxy/transport-fn.js +55 -0
  77. package/dist/proxy/transport.d.ts +0 -25
  78. package/dist/proxy/transport.js +0 -38
  79. package/dist/upgrade/checker.d.ts +1 -1
  80. package/dist/upgrade/checker.js +16 -1
  81. package/dist/upgrade/deployment.js +2 -2
  82. package/dist/utils/password.js +4 -2
  83. package/dist/utils/time-range.d.ts +9 -0
  84. package/dist/utils/time-range.js +40 -0
  85. package/frontend-dist/assets/{CardContent-Deyvo1TQ.js → CardContent-WrBnGhTg.js} +1 -1
  86. package/frontend-dist/assets/{CardTitle-DujSYXja.js → CardTitle-BcDYk7cq.js} +1 -1
  87. package/frontend-dist/assets/Checkbox-MZf0YsDG.js +1 -0
  88. package/frontend-dist/assets/{CollapsibleTrigger-ByCvAsW0.js → CollapsibleTrigger-CrOH9HlW.js} +1 -1
  89. package/frontend-dist/assets/{Collection-V6gcBlwC.js → Collection-DcTx_Y54.js} +1 -1
  90. package/frontend-dist/assets/Dashboard-D0oDrSLr.js +3 -0
  91. package/frontend-dist/assets/{DialogTitle-D0nwX87v.js → DialogTitle-Cl5Cd7QH.js} +1 -1
  92. package/frontend-dist/assets/{Input-D0kpZB31.js → Input-O0ebU-Va.js} +1 -1
  93. package/frontend-dist/assets/{Label-BvYK0rd6.js → Label-C_S0y7Um.js} +1 -1
  94. package/frontend-dist/assets/Login-DGY7uF8P.js +1 -0
  95. package/frontend-dist/assets/Logs-ls8pv89b.js +1 -0
  96. package/frontend-dist/assets/{ModelMappings-BoG2P9Rh.js → ModelMappings-DGlf0S4s.js} +1 -1
  97. package/frontend-dist/assets/{Monitor-W441wik3.js → Monitor-BSI87grz.js} +1 -1
  98. package/frontend-dist/assets/{PopperContent-DVJ4IxLF.js → PopperContent-C6Q7hDmf.js} +1 -1
  99. package/frontend-dist/assets/{Providers-D2rzb_Qk.js → Providers-ZkRpj8_m.js} +1 -1
  100. package/frontend-dist/assets/ProxyEnhancement-DFPI1W6Z.js +5 -0
  101. package/frontend-dist/assets/RetryRules-DtM31qsl.js +1 -0
  102. package/frontend-dist/assets/RouterKeys-D63tRFKm.js +1 -0
  103. package/frontend-dist/assets/RovingFocusItem-BJoylAKU.js +1 -0
  104. package/frontend-dist/assets/{SelectValue-CAEBdE04.js → SelectValue-CLp5z6_I.js} +1 -1
  105. package/frontend-dist/assets/{Settings-3lR8QVQt.js → Settings-DSgRKbTQ.js} +2 -2
  106. package/frontend-dist/assets/Setup-BDmj6CRk.js +1 -0
  107. package/frontend-dist/assets/{Switch-CST3045A.js → Switch-Wz-t_zkv.js} +1 -1
  108. package/frontend-dist/assets/TableHeader-DGtcqGkw.js +1 -0
  109. package/frontend-dist/assets/TabsTrigger-CPCi2HIa.js +1 -0
  110. package/frontend-dist/assets/{Teleport-DVgMe9KS.js → Teleport-DdjYHlNK.js} +1 -1
  111. package/frontend-dist/assets/TooltipTrigger-H_QoPY1n.js +1 -0
  112. package/frontend-dist/assets/{UnifiedRequestDialog-Fe2TfhTD.js → UnifiedRequestDialog-BAAfMJJl.js} +1 -1
  113. package/frontend-dist/assets/{VisuallyHidden-CjuTDGlC.js → VisuallyHidden-Cyk-jWwh.js} +1 -1
  114. package/frontend-dist/assets/{VisuallyHiddenInput-BaW-2aEF.js → VisuallyHiddenInput-CYjNe_H8.js} +1 -1
  115. package/frontend-dist/assets/{alert-dialog-Bv6dVarS.js → alert-dialog-Bi3dliLl.js} +1 -1
  116. package/frontend-dist/assets/{badge-CEfcely6.js → badge-Kkta3e9W.js} +1 -1
  117. package/frontend-dist/assets/{button-BmxhlpN-.js → button-BQ3s7yNh.js} +2 -2
  118. package/frontend-dist/assets/{createLucideIcon-UWoYUKtZ.js → createLucideIcon-D1tkPDOQ.js} +1 -1
  119. package/frontend-dist/assets/{dialog-QaGxKbze.js → dialog-DoIATUYw.js} +1 -1
  120. package/frontend-dist/assets/{file-text-D38GtYz2.js → file-text-Dt6QP1bZ.js} +1 -1
  121. package/frontend-dist/assets/{index-D484ZFa9.js → index-BY0E7CHR.js} +1 -1
  122. package/frontend-dist/assets/index-Bnrh1mFY.css +1 -0
  123. package/frontend-dist/assets/{lib-CSYRBKqn.js → lib-CxwxnlwW.js} +1 -1
  124. package/frontend-dist/assets/{ohash.D__AXeF1-BUMsW586.js → ohash.D__AXeF1-b0PiKZB_.js} +1 -1
  125. package/frontend-dist/assets/{useClipboard-CuE5xXIg.js → useClipboard-Cnnz6AAN.js} +1 -1
  126. package/frontend-dist/assets/useLogRetention-DYP5LOAc.js +1 -0
  127. package/frontend-dist/assets/useNonce-DKbOCfgM.js +1 -0
  128. package/frontend-dist/assets/x-CAoitXRt.js +1 -0
  129. package/frontend-dist/index.html +18 -18
  130. package/package.json +2 -1
  131. package/dist/proxy/directive-parser.d.ts +0 -7
  132. package/frontend-dist/assets/Checkbox-BJxf-QuV.js +0 -1
  133. package/frontend-dist/assets/Dashboard-xqf6PcmE.js +0 -3
  134. package/frontend-dist/assets/Login-C9oPKRcu.js +0 -1
  135. package/frontend-dist/assets/Logs-DVgenFav.js +0 -1
  136. package/frontend-dist/assets/ProxyEnhancement-DahQkV1g.js +0 -5
  137. package/frontend-dist/assets/RetryRules-Bg9p50oc.js +0 -1
  138. package/frontend-dist/assets/RouterKeys-C1LhXbqf.js +0 -1
  139. package/frontend-dist/assets/Setup-Dzj1XvgF.js +0 -1
  140. package/frontend-dist/assets/TableHeader-CIrxcNRh.js +0 -1
  141. package/frontend-dist/assets/TabsContent-B4nroq3-.js +0 -1
  142. package/frontend-dist/assets/TabsTrigger-FsELRpyc.js +0 -1
  143. package/frontend-dist/assets/index-CMBzqUyT.css +0 -1
  144. package/frontend-dist/assets/useLogRetention-DesMKwIU.js +0 -1
  145. package/frontend-dist/assets/useNonce-FLqOooWA.js +0 -1
  146. package/frontend-dist/assets/x-BEUXSxcj.js +0 -1
  147. /package/dist/proxy/{enhancement-handler.d.ts → enhancement/enhancement-handler.d.ts} +0 -0
  148. /package/dist/proxy/{response-cleaner.d.ts → enhancement/response-cleaner.d.ts} +0 -0
  149. /package/frontend-dist/assets/{format-CPdJtjZ5.js → format-DOVIVsQC.js} +0 -0
@@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
3
3
  import { getSetting, setSetting, isInitialized } from "../db/settings.js";
4
4
  import { hashPassword } from "../utils/password.js";
5
5
  import { HTTP_BAD_REQUEST, HTTP_CONFLICT } from "./constants.js";
6
+ import { API_CODE, apiError } from "./api-response.js";
6
7
  const CRYPTO_BYTES_LENGTH = 32;
7
8
  const MIN_PASSWORD_LENGTH = 6;
8
9
  export const adminSetupRoutes = (app, options, done) => {
@@ -13,7 +14,7 @@ export const adminSetupRoutes = (app, options, done) => {
13
14
  app.post("/admin/api/setup/initialize", async (request, reply) => {
14
15
  const { password } = request.body;
15
16
  if (!password || password.length < MIN_PASSWORD_LENGTH) {
16
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: `Password must be at least ${MIN_PASSWORD_LENGTH} characters` } });
17
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, `Password must be at least ${MIN_PASSWORD_LENGTH} characters`));
17
18
  }
18
19
  // 事务中原子检查防竞态
19
20
  const alreadyInitialized = db.transaction(() => {
@@ -28,7 +29,7 @@ export const adminSetupRoutes = (app, options, done) => {
28
29
  return false;
29
30
  })();
30
31
  if (alreadyInitialized) {
31
- return reply.code(HTTP_CONFLICT).send({ error: { message: "Already initialized" } });
32
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.ALREADY_INITIALIZED, "Already initialized"));
32
33
  }
33
34
  // 自动登录:签发 JWT
34
35
  const TOKEN_EXPIRY_SECONDS = 172800; // 48 hours,与 admin-auth 保持一致
@@ -1,14 +1,31 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getStats } from "../db/index.js";
3
+ import { resolveTimeRange } from "../utils/time-range.js";
3
4
  const StatsQuerySchema = Type.Object({
4
- period: Type.Optional(Type.String()),
5
+ period: Type.Optional(Type.Union([
6
+ Type.Literal("window"),
7
+ Type.Literal("weekly"),
8
+ Type.Literal("monthly"),
9
+ ])),
10
+ start_time: Type.Optional(Type.String()),
11
+ end_time: Type.Optional(Type.String()),
5
12
  router_key_id: Type.Optional(Type.String()),
6
13
  });
7
14
  export const adminStatsRoutes = (app, options, done) => {
8
15
  app.get("/admin/api/stats", { schema: { querystring: StatsQuerySchema } }, async (request, reply) => {
9
16
  const query = request.query;
10
- const period = (query.period || "24h");
11
- const stats = getStats(options.db, period, query.router_key_id);
17
+ let startTime;
18
+ let endTime;
19
+ if (query.start_time && query.end_time) {
20
+ startTime = query.start_time;
21
+ endTime = query.end_time;
22
+ }
23
+ else {
24
+ const range = resolveTimeRange((query.period ?? "weekly"), options.db, query.router_key_id);
25
+ startTime = range.startTime;
26
+ endTime = range.endTime;
27
+ }
28
+ const stats = getStats(options.db, startTime, endTime, query.router_key_id);
12
29
  return reply.send(stats);
13
30
  });
14
31
  done();
@@ -6,6 +6,7 @@ import { execSync } from 'node:child_process';
6
6
  import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { HTTP_BAD_REQUEST, HTTP_INTERNAL_ERROR } from '../constants.js';
9
+ import { API_CODE, apiError } from './api-response.js';
9
10
  const GITHUB_CONFIG_BASE = 'https://raw.githubusercontent.com/zhushanwen321/llm-simple-router/main/config';
10
11
  const GITEE_CONFIG_BASE = 'https://gitee.com/zzzzswszzzz/llm-simple-router/raw/main/config';
11
12
  const CHECK_INTERVAL_MS = 60 * 60 * 1000; // eslint-disable-line no-magic-numbers
@@ -48,7 +49,7 @@ export const adminUpgradeRoutes = (app, options, done) => {
48
49
  app.put('/admin/api/upgrade/sync-source', async (req, reply) => {
49
50
  const { source } = req.body;
50
51
  if (source !== 'github' && source !== 'gitee') {
51
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: 'source must be github or gitee' } });
52
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, 'source must be github or gitee'));
52
53
  }
53
54
  setConfigSyncSource(db, source);
54
55
  return reply.send({ ok: true });
@@ -56,14 +57,14 @@ export const adminUpgradeRoutes = (app, options, done) => {
56
57
  app.post('/admin/api/upgrade/execute', async (req, reply) => {
57
58
  const deployment = detectDeployment();
58
59
  if (deployment !== 'npm') {
59
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: '仅支持 npm 全局安装模式下自动升级' } });
60
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, '仅支持 npm 全局安装模式下自动升级'));
60
61
  }
61
62
  const { version } = req.body;
62
63
  if (!version) {
63
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: 'version is required' } });
64
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, 'version is required'));
64
65
  }
65
66
  if (!/^\d+\.\d+\.\d+$/.test(version)) {
66
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: '无效版本号格式' } });
67
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, '无效版本号格式'));
67
68
  }
68
69
  try {
69
70
  execSync(`npm install -g llm-simple-router@${version}`, {
@@ -74,13 +75,13 @@ export const adminUpgradeRoutes = (app, options, done) => {
74
75
  }
75
76
  catch (err) {
76
77
  const msg = err instanceof Error ? err.message : String(err);
77
- return reply.code(HTTP_INTERNAL_ERROR).send({ error: { message: `升级失败: ${msg}` } });
78
+ return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `升级失败: ${msg}`));
78
79
  }
79
80
  });
80
81
  app.post('/admin/api/upgrade/sync-config', async (req, reply) => {
81
82
  const { source } = req.body;
82
83
  if (source !== 'github' && source !== 'gitee') {
83
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: 'source must be github or gitee' } });
84
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, 'source must be github or gitee'));
84
85
  }
85
86
  const base = getConfigBaseUrl(source);
86
87
  const configDir = path.resolve(process.cwd(), 'config');
@@ -106,7 +107,7 @@ export const adminUpgradeRoutes = (app, options, done) => {
106
107
  }
107
108
  catch (err) {
108
109
  const msg = err instanceof Error ? err.message : String(err);
109
- return reply.code(HTTP_INTERNAL_ERROR).send({ error: { message: `同步失败: ${msg}` } });
110
+ return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `同步失败: ${msg}`));
110
111
  }
111
112
  });
112
113
  done();
@@ -1,24 +1,16 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getWindowsInRange, getWindowUsage } from "../db/usage-windows.js";
3
- import { toSqliteDatetime } from "../utils/datetime.js";
3
+ import { resolveTimeRange } from "../utils/time-range.js";
4
4
  const UsageQuerySchema = Type.Object({
5
5
  router_key_id: Type.Optional(Type.String()),
6
6
  });
7
- function getMonday(date) {
8
- const d = new Date(date);
9
- const day = d.getDay();
10
- // 周日 getDay()=0,需要回退到上周一;其余日期减到周一
11
- const diff = d.getDate() - day + (day === 0 ? -6 : 1); // eslint-disable-line no-magic-numbers
12
- d.setDate(diff);
13
- return d;
14
- }
15
- function getDailyUsage(db, start, end, routerKeyId) {
7
+ function getDailyUsage(db, startTime, endTime, routerKeyId) {
16
8
  const routerKeyFilter = routerKeyId
17
9
  ? " AND rl.router_key_id = ?"
18
10
  : "";
19
11
  const params = routerKeyId
20
- ? [toSqliteDatetime(start), toSqliteDatetime(end), routerKeyId]
21
- : [toSqliteDatetime(start), toSqliteDatetime(end)];
12
+ ? [startTime, endTime, routerKeyId]
13
+ : [startTime, endTime];
22
14
  return db.prepare(`
23
15
  SELECT
24
16
  date(rm.created_at) AS date,
@@ -39,11 +31,10 @@ export const adminUsageRoutes = (app, options, done) => {
39
31
  const { db } = options;
40
32
  app.get("/admin/api/usage/windows", { schema: { querystring: UsageQuerySchema } }, async (request) => {
41
33
  const query = request.query;
42
- const today = new Date();
43
- today.setHours(0, 0, 0, 0);
44
- const tomorrow = new Date(today);
45
- tomorrow.setDate(tomorrow.getDate() + 1);
46
- const windows = getWindowsInRange(db, toSqliteDatetime(today), toSqliteDatetime(tomorrow), query.router_key_id);
34
+ const range = resolveTimeRange("window", db, query.router_key_id);
35
+ const windows = getWindowsInRange(db, range.startTime, range.endTime, query.router_key_id);
36
+ if (windows.length === 0)
37
+ return [];
47
38
  return windows.map(w => ({
48
39
  window: w,
49
40
  usage: getWindowUsage(db, w.start_time, w.end_time, query.router_key_id),
@@ -51,16 +42,13 @@ export const adminUsageRoutes = (app, options, done) => {
51
42
  });
52
43
  app.get("/admin/api/usage/weekly", { schema: { querystring: UsageQuerySchema } }, async (request) => {
53
44
  const query = request.query;
54
- const now = new Date();
55
- const monday = getMonday(now);
56
- monday.setHours(0, 0, 0, 0);
57
- return getDailyUsage(db, monday, now, query.router_key_id);
45
+ const range = resolveTimeRange("weekly", db, query.router_key_id);
46
+ return getDailyUsage(db, range.startTime, range.endTime, query.router_key_id);
58
47
  });
59
48
  app.get("/admin/api/usage/monthly", { schema: { querystring: UsageQuerySchema } }, async (request) => {
60
49
  const query = request.query;
61
- const now = new Date();
62
- const firstOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
63
- return getDailyUsage(db, firstOfMonth, now, query.router_key_id);
50
+ const range = resolveTimeRange("monthly", db, query.router_key_id);
51
+ return getDailyUsage(db, range.startTime, range.endTime, query.router_key_id);
64
52
  });
65
53
  done();
66
54
  };
package/dist/config.d.ts CHANGED
@@ -4,9 +4,9 @@ export interface Config {
4
4
  LOG_LEVEL: string;
5
5
  TZ: string;
6
6
  STREAM_TIMEOUT_MS: number;
7
- RETRY_MAX_ATTEMPTS: number;
8
7
  RETRY_BASE_DELAY_MS: number;
9
8
  }
10
9
  export declare function resetConfig(): void;
11
10
  export declare function getBaseConfig(): Config;
11
+ /** @deprecated Use getBaseConfig directly */
12
12
  export declare function getConfig(): Config;
package/dist/config.js CHANGED
@@ -18,11 +18,11 @@ export function getBaseConfig() {
18
18
  LOG_LEVEL: process.env.LOG_LEVEL || "info",
19
19
  TZ: process.env.TZ || "Asia/Shanghai",
20
20
  STREAM_TIMEOUT_MS: parseInt(process.env.STREAM_TIMEOUT_MS || "3000000", 10),
21
- RETRY_MAX_ATTEMPTS: parseInt(process.env.RETRY_MAX_ATTEMPTS || "3", 10),
22
21
  RETRY_BASE_DELAY_MS: parseInt(process.env.RETRY_BASE_DELAY_MS || "1000", 10),
23
22
  };
24
23
  return cachedConfig;
25
24
  }
25
+ /** @deprecated Use getBaseConfig directly */
26
26
  export function getConfig() {
27
27
  return getBaseConfig();
28
28
  }
@@ -6,3 +6,6 @@ export declare const HTTP_CONFLICT = 409;
6
6
  export declare const HTTP_INTERNAL_ERROR = 500;
7
7
  export declare const HTTP_BAD_GATEWAY = 502;
8
8
  export declare const HTTP_SERVICE_UNAVAILABLE = 503;
9
+ export declare const PROXY_API_TYPES: Record<string, string>;
10
+ export declare function getProxyApiType(url: string): string | null;
11
+ export declare const MS_PER_SECOND = 1000;
package/dist/constants.js CHANGED
@@ -7,3 +7,14 @@ export const HTTP_CONFLICT = 409;
7
7
  export const HTTP_INTERNAL_ERROR = 500;
8
8
  export const HTTP_BAD_GATEWAY = 502;
9
9
  export const HTTP_SERVICE_UNAVAILABLE = 503;
10
+ // api_type 路由映射:proxy path → api type,用于全局 hook/errorHandler 中识别代理请求
11
+ export const PROXY_API_TYPES = {
12
+ "/v1/chat/completions": "openai",
13
+ "/v1/models": "openai",
14
+ "/v1/messages": "anthropic",
15
+ };
16
+ export function getProxyApiType(url) {
17
+ const path = url.split("?")[0];
18
+ return PROXY_API_TYPES[path] ?? null;
19
+ }
20
+ export const MS_PER_SECOND = 1000;
@@ -1,10 +1,10 @@
1
1
  import Database from "better-sqlite3";
2
2
  export declare function initDatabase(dbPath: string): Database.Database;
3
- export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
3
+ export { getActiveProviders, getAllProviders, getProviderById, getActiveProviderByName, getActiveProvidersWithModels, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
4
4
  export type { Provider } from "./providers.js";
5
5
  export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
6
6
  export type { ModelMapping, MappingGroup, ProviderModelEntry } from "./mappings.js";
7
- export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
7
+ export { getActiveRetryRules, getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
8
8
  export type { RetryRule } from "./retry-rules.js";
9
9
  export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogMetrics, updateLogStreamContent, backfillMetricsFromRequestMetrics, estimateLogTableSize, deleteOldestLogs, getLogCount, } from "./logs.js";
10
10
  export type { RequestLog, RequestLogGroupedRow, RequestLogListRow } from "./logs.js";
@@ -13,7 +13,7 @@ export type { RouterKey } from "./router-keys.js";
13
13
  export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
14
14
  export type { MetricsSummaryRow, MetricsTimeseriesRow, MetricsPeriod, MetricsMetric, MetricsRow, MetricsInsert } from "./metrics.js";
15
15
  export { getStats } from "./stats.js";
16
- export type { Stats, StatsPeriod } from "./stats.js";
16
+ export type { Stats } from "./stats.js";
17
17
  export { getSetting, setSetting, isInitialized } from "./settings.js";
18
18
  export { getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, } from "./settings.js";
19
19
  export { getSessionStates, getSessionState, getSessionHistory, upsertSessionState, insertSessionHistory, deleteSessionState, } from "./session-states.js";
package/dist/db/index.js CHANGED
@@ -53,9 +53,9 @@ export function initDatabase(dbPath) {
53
53
  return db;
54
54
  }
55
55
  // --- Re-export from per-table modules ---
56
- export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
56
+ export { getActiveProviders, getAllProviders, getProviderById, getActiveProviderByName, getActiveProvidersWithModels, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
57
57
  export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
58
- export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
58
+ export { getActiveRetryRules, getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
59
59
  export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogMetrics, updateLogStreamContent, backfillMetricsFromRequestMetrics, estimateLogTableSize, deleteOldestLogs, getLogCount, } from "./logs.js";
60
60
  export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
61
61
  export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "crypto";
2
+ import { isTarget } from "../proxy/strategy/targets-rule.js";
2
3
  import { buildUpdateQuery, deleteById } from "./helpers.js";
3
4
  const MAPPING_FIELDS = new Set(["client_model", "backend_model", "provider_id", "is_active"]);
4
5
  const GROUP_FIELDS = new Set(["client_model", "strategy", "rule"]);
@@ -70,24 +71,20 @@ export function getActiveProviderModels(db) {
70
71
  }
71
72
  return results;
72
73
  }
73
- function isTargetLike(obj) {
74
- return typeof obj === "object" && obj !== null &&
75
- typeof obj.backend_model === "string" &&
76
- typeof obj.provider_id === "string";
77
- }
74
+ // --- 从 mapping_groups rule JSON 中提取 target 条目 ---
78
75
  function extractTargets(rule) {
79
76
  const results = [];
80
- if (isTargetLike(rule.default))
77
+ if (isTarget(rule.default))
81
78
  results.push(rule.default);
82
79
  if (Array.isArray(rule.targets)) {
83
80
  for (const t of rule.targets) {
84
- if (isTargetLike(t))
81
+ if (isTarget(t))
85
82
  results.push(t);
86
83
  }
87
84
  }
88
85
  if (Array.isArray(rule.windows)) {
89
86
  for (const w of rule.windows) {
90
- if (w && typeof w === "object" && isTargetLike(w.target)) {
87
+ if (w && typeof w === "object" && isTarget(w.target)) {
91
88
  results.push(w.target);
92
89
  }
93
90
  }
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "crypto";
2
+ import { MS_PER_SECOND } from "../constants.js";
2
3
  export function insertMetrics(db, m) {
3
4
  const id = randomUUID();
4
5
  db.prepare(`INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ttft_ms, total_duration_ms, tokens_per_second, stop_reason, is_complete)
@@ -21,8 +22,6 @@ const BUCKET_SECONDS = {
21
22
  "7d": 3600,
22
23
  "30d": 14400,
23
24
  };
24
- // unix epoch 秒转毫秒的乘数
25
- const MS_PER_SEC = 1000;
26
25
  // 时间跨度(秒)→ 桶大小(秒)的阶梯映射,与 BUCKET_SECONDS 保持对齐
27
26
  const BUCKET_THRESHOLDS = [
28
27
  { maxSec: 3600, bucketSec: 60 }, // ≤1h: 1min
@@ -33,7 +32,7 @@ const BUCKET_THRESHOLDS = [
33
32
  const FALLBACK_BUCKET_SEC = 14400; // >7d: 4h
34
33
  function calculateBucketSeconds(startTime, endTime) {
35
34
  const ms = new Date(endTime).getTime() - new Date(startTime).getTime();
36
- const sec = ms / MS_PER_SEC;
35
+ const sec = ms / MS_PER_SECOND;
37
36
  const match = BUCKET_THRESHOLDS.find((t) => sec <= t.maxSec);
38
37
  return match ? match.bucketSec : FALLBACK_BUCKET_SEC;
39
38
  }
@@ -127,7 +126,7 @@ export function getMetricsTimeseries(db, period, metric, providerId, backendMode
127
126
  ORDER BY bucket_key ASC
128
127
  `).all(bucketSec, bucketSec, ...params);
129
128
  return rows.map((r) => ({
130
- time_bucket: new Date(r.bucket_key * MS_PER_SEC).toISOString(),
129
+ time_bucket: new Date(r.bucket_key * MS_PER_SECOND).toISOString(),
131
130
  avg_value: r.avg_value,
132
131
  count: r.count,
133
132
  }));
@@ -36,3 +36,11 @@ export declare function createProvider(db: Database.Database, provider: {
36
36
  }): string;
37
37
  export declare function updateProvider(db: Database.Database, id: string, fields: Partial<Pick<Provider, "name" | "api_type" | "base_url" | "api_key" | "api_key_preview" | "models" | "is_active" | "max_concurrency" | "queue_timeout_ms" | "max_queue_size">>): void;
38
38
  export declare function deleteProvider(db: Database.Database, id: string): void;
39
+ export declare function getActiveProviderByName(db: Database.Database, name: string): {
40
+ id: string;
41
+ models: string;
42
+ } | undefined;
43
+ export declare function getActiveProvidersWithModels(db: Database.Database): {
44
+ id: string;
45
+ models: string;
46
+ }[];
@@ -32,3 +32,9 @@ export function updateProvider(db, id, fields) {
32
32
  export function deleteProvider(db, id) {
33
33
  deleteById(db, "providers", id);
34
34
  }
35
+ export function getActiveProviderByName(db, name) {
36
+ return db.prepare("SELECT id, models FROM providers WHERE name = ? AND is_active = 1").get(name);
37
+ }
38
+ export function getActiveProvidersWithModels(db) {
39
+ return db.prepare("SELECT id, models FROM providers WHERE is_active = 1").all();
40
+ }
@@ -25,3 +25,4 @@ export declare function createRetryRule(db: Database.Database, rule: {
25
25
  }): string;
26
26
  export declare function updateRetryRule(db: Database.Database, id: string, fields: Partial<Pick<RetryRule, "name" | "status_code" | "body_pattern" | "is_active" | "retry_strategy" | "retry_delay_ms" | "max_retries" | "max_delay_ms">>): void;
27
27
  export declare function deleteRetryRule(db: Database.Database, id: string): void;
28
+ export declare function getRetryRuleById(db: Database.Database, id: string): RetryRule | undefined;
@@ -27,3 +27,6 @@ export function updateRetryRule(db, id, fields) {
27
27
  export function deleteRetryRule(db, id) {
28
28
  deleteById(db, "retry_rules", id);
29
29
  }
30
+ export function getRetryRuleById(db, id) {
31
+ return db.prepare("SELECT * FROM retry_rules WHERE id = ?").get(id);
32
+ }
@@ -1,9 +1,8 @@
1
1
  import Database from "better-sqlite3";
2
- export type StatsPeriod = "1h" | "6h" | "24h" | "7d" | "30d";
3
2
  export interface Stats {
4
3
  totalRequests: number;
5
4
  successRate: number;
6
5
  avgTps: number;
7
6
  totalTokens: number;
8
7
  }
9
- export declare function getStats(db: Database.Database, period: StatsPeriod, routerKeyId?: string): Stats;
8
+ export declare function getStats(db: Database.Database, startTime: string, endTime: string, routerKeyId?: string): Stats;
package/dist/db/stats.js CHANGED
@@ -1,14 +1,10 @@
1
- const PERIOD_OFFSET = {
2
- "1h": "-1 hours",
3
- "6h": "-6 hours",
4
- "24h": "-1 day",
5
- "7d": "-7 days",
6
- "30d": "-30 days",
7
- };
8
- export function getStats(db, period, routerKeyId) {
9
- const offset = PERIOD_OFFSET[period];
10
- const conditions = ["rm.is_complete = 1", "rm.created_at >= datetime('now', ?)"];
11
- const params = [offset];
1
+ export function getStats(db, startTime, endTime, routerKeyId) {
2
+ const conditions = [
3
+ "rm.is_complete = 1",
4
+ "rm.created_at >= datetime(?)",
5
+ "rm.created_at < datetime(?)",
6
+ ];
7
+ const params = [startTime, endTime];
12
8
  if (routerKeyId) {
13
9
  conditions.push("rl.router_key_id = ?");
14
10
  params.push(routerKeyId);
package/dist/index.js CHANGED
@@ -5,19 +5,10 @@ import { existsSync } from "node:fs";
5
5
  import { randomUUID } from "crypto";
6
6
  import Fastify from "fastify";
7
7
  import { insertRequestLog } from "./db/logs.js";
8
- import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR, HTTP_BAD_REQUEST } from "./constants.js";
8
+ import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR, getProxyApiType } from "./constants.js";
9
+ import { API_CODE, apiError, isAdminApiResponse, statusToApiCode } from "./admin/api-response.js";
9
10
  const PROVIDER_DEFAULT_QUEUE_TIMEOUT_MS = 5000;
10
11
  const PROVIDER_DEFAULT_MAX_QUEUE_SIZE = 100;
11
- // 代理路由路径 → api_type,用于在全局 hook/errorHandler 中识别代理请求
12
- const PROXY_API_TYPES = {
13
- "/v1/chat/completions": "openai",
14
- "/v1/messages": "anthropic",
15
- "/v1/models": "openai",
16
- };
17
- function getProxyApiType(url) {
18
- const path = url.split("?")[0];
19
- return PROXY_API_TYPES[path] ?? null;
20
- }
21
12
  const __filename = fileURLToPath(import.meta.url);
22
13
  const __dirname = path.dirname(__filename);
23
14
  import { getConfig } from "./config.js";
@@ -80,32 +71,59 @@ export async function buildApp(options) {
80
71
  .join("; ");
81
72
  return new Error(message);
82
73
  });
83
- // 统一 schema validation 错误响应格式,代理路由的错误也记录到 request_logs
74
+ // 统一错误处理:代理路由保持 {error:{message}},Admin API 使用信封格式
84
75
  app.setErrorHandler((error, request, reply) => {
85
76
  const fastifyError = error;
86
77
  const status = fastifyError.statusCode ?? HTTP_INTERNAL_ERROR;
87
- const proxyApiType = getProxyApiType(request.url);
88
- if (proxyApiType) {
89
- request.log.error({ statusCode: status, err: error }, `Proxy request error: ${fastifyError.message}`);
90
- const body = request.body;
91
- insertRequestLog(db, {
92
- id: randomUUID(),
93
- api_type: proxyApiType,
94
- model: body?.model || null,
95
- provider_id: null,
96
- status_code: status,
97
- latency_ms: 0,
98
- is_stream: 0,
99
- error_message: fastifyError.message,
100
- created_at: new Date().toISOString(),
101
- client_request: JSON.stringify({ headers: request.headers }),
102
- router_key_id: request.routerKey?.id ?? null,
103
- });
78
+ // 代理路由保持原有格式,并记录到 request_logs
79
+ if (!isAdminApiResponse(request.url)) {
80
+ const proxyApiType = getProxyApiType(request.url);
81
+ if (proxyApiType) {
82
+ request.log.error({ statusCode: status, err: error }, `Proxy request error: ${fastifyError.message}`);
83
+ const body = request.body;
84
+ insertRequestLog(db, {
85
+ id: randomUUID(),
86
+ api_type: proxyApiType,
87
+ model: body?.model || null,
88
+ provider_id: null,
89
+ status_code: status,
90
+ latency_ms: 0,
91
+ is_stream: 0,
92
+ error_message: fastifyError.message,
93
+ created_at: new Date().toISOString(),
94
+ client_request: JSON.stringify({ headers: request.headers }),
95
+ router_key_id: request.routerKey?.id ?? null,
96
+ });
97
+ }
98
+ return reply.code(status).send({ error: { message: fastifyError.message } });
104
99
  }
105
- if (status === HTTP_BAD_REQUEST && fastifyError.validation) {
106
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: fastifyError.message } });
100
+ // Admin API 统一信封错误格式
101
+ const code = statusToApiCode(status);
102
+ return reply.code(status).send(apiError(code, fastifyError.message));
103
+ });
104
+ // onSend hook:自动包装 Admin API 成功响应为信封格式
105
+ app.addHook('onSend', async (request, reply, payload) => {
106
+ if (!isAdminApiResponse(request.url, reply.getHeader('content-type'))) {
107
+ return payload;
107
108
  }
108
- return reply.code(status).send({ error: { message: fastifyError.message } });
109
+ // 已是错误信封(errorHandler 已包装)或已是信封格式 跳过
110
+ if (typeof payload === 'string') {
111
+ try {
112
+ const parsed = JSON.parse(payload);
113
+ if ('code' in parsed)
114
+ return payload; // errorHandler 或路由已手动包装
115
+ }
116
+ catch {
117
+ return payload;
118
+ }
119
+ }
120
+ // 包装成功响应
121
+ const wrapped = {
122
+ code: API_CODE.SUCCESS,
123
+ message: 'ok',
124
+ data: typeof payload === 'string' ? JSON.parse(payload) : payload,
125
+ };
126
+ return JSON.stringify(wrapped);
109
127
  });
110
128
  loadRecommendedConfig();
111
129
  startUpgradeChecker(options?.upgradeCheckerOptions);
@@ -147,20 +165,20 @@ export async function buildApp(options) {
147
165
  app.register(openaiProxy, {
148
166
  db,
149
167
  streamTimeoutMs: config.STREAM_TIMEOUT_MS,
150
- retryMaxAttempts: config.RETRY_MAX_ATTEMPTS,
151
168
  retryBaseDelayMs: config.RETRY_BASE_DELAY_MS,
152
169
  matcher,
153
170
  semaphoreManager,
154
171
  tracker,
172
+ usageWindowTracker,
155
173
  });
156
174
  app.register(anthropicProxy, {
157
175
  db,
158
176
  streamTimeoutMs: config.STREAM_TIMEOUT_MS,
159
- retryMaxAttempts: config.RETRY_MAX_ATTEMPTS,
160
177
  retryBaseDelayMs: config.RETRY_BASE_DELAY_MS,
161
178
  matcher,
162
179
  semaphoreManager,
163
180
  tracker,
181
+ usageWindowTracker,
164
182
  });
165
183
  app.register(adminRoutes, { db, matcher, tracker, semaphoreManager });
166
184
  // 前端静态文件服务(生产环境)
@@ -1,4 +1,4 @@
1
- const MS_PER_SECOND = 1000;
1
+ import { MS_PER_SECOND } from "../constants.js";
2
2
  export class MetricsExtractor {
3
3
  apiType;
4
4
  requestStartTime;
@@ -5,6 +5,8 @@ export class SSEParser {
5
5
  if (this.isDone)
6
6
  return [];
7
7
  this.buffer += chunk;
8
+ // SSE 规范允许 \r\n 行尾,统一为 \n
9
+ this.buffer = this.buffer.replace(/\r\n/g, "\n");
8
10
  return this.drainEvents();
9
11
  }
10
12
  flush() {
@@ -3,6 +3,7 @@ import cookie from "@fastify/cookie";
3
3
  import jwt from "jsonwebtoken";
4
4
  import { isInitialized, getSetting } from "../db/settings.js";
5
5
  import { verifyPassword } from "../utils/password.js";
6
+ import { API_CODE, apiError } from "../admin/api-response.js";
6
7
  const HTTP_UNAUTHORIZED = 401;
7
8
  const adminAuthRaw = (app, options, done) => {
8
9
  app.register(cookie);
@@ -19,11 +20,11 @@ const adminAuthRaw = (app, options, done) => {
19
20
  return;
20
21
  // 未初始化时返回 needsSetup
21
22
  if (!isInitialized(options.db)) {
22
- return reply.code(HTTP_UNAUTHORIZED).send({ error: { message: "Not initialized", needsSetup: true } });
23
+ return reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.NOT_INITIALIZED, "Not initialized"));
23
24
  }
24
25
  const token = request.cookies["admin_token"];
25
26
  if (!token) {
26
- reply.code(HTTP_UNAUTHORIZED).send({ error: { message: "Not authenticated" } });
27
+ reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.TOKEN_INVALID, "Not authenticated"));
27
28
  return reply;
28
29
  }
29
30
  const secret = getSetting(options.db, "jwt_secret");
@@ -32,7 +33,7 @@ const adminAuthRaw = (app, options, done) => {
32
33
  }
33
34
  catch (err) {
34
35
  request.log.debug({ err }, "invalid JWT token");
35
- reply.code(HTTP_UNAUTHORIZED).send({ error: { message: "Invalid or expired token" } });
36
+ reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.TOKEN_INVALID, "Invalid or expired token"));
36
37
  return reply;
37
38
  }
38
39
  });
@@ -44,12 +45,12 @@ export const adminLoginRoutes = (app, options, done) => {
44
45
  app.post("/admin/api/login", async (request, reply) => {
45
46
  const { password } = request.body;
46
47
  if (!password) {
47
- return reply.code(HTTP_UNAUTHORIZED).send({ error: { message: "Invalid password" } });
48
+ return reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.WRONG_PASSWORD, "Invalid password"));
48
49
  }
49
50
  // DB 模式:scrypt hash 验证
50
51
  const hash = getSetting(options.db, "admin_password_hash");
51
52
  if (!hash || !verifyPassword(password, hash)) {
52
- return reply.code(HTTP_UNAUTHORIZED).send({ error: { message: "Invalid password" } });
53
+ return reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.WRONG_PASSWORD, "Invalid password"));
53
54
  }
54
55
  const secret = getSetting(options.db, "jwt_secret");
55
56
  const token = jwt.sign({ role: "admin" }, secret, { expiresIn: TOKEN_EXPIRY_SECONDS });
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "crypto";
2
2
  import fp from "fastify-plugin";
3
3
  import { isInitialized } from "../db/settings.js";
4
4
  import { insertRequestLog } from "../db/logs.js";
5
+ import { getProxyApiType } from "../constants.js";
5
6
  const SKIP_PATHS = ["/health", "/admin"];
6
7
  const HTTP_UNAUTHORIZED = 401;
7
8
  const HTTP_SERVICE_UNAVAILABLE = 503;
@@ -19,16 +20,6 @@ function unauthorizedReply(reply) {
19
20
  },
20
21
  });
21
22
  }
22
- // 代理路由路径 → api_type 映射,用于记录被认证拒绝的请求
23
- const PROXY_API_TYPES = {
24
- "/v1/chat/completions": "openai",
25
- "/v1/messages": "anthropic",
26
- "/v1/models": "openai",
27
- };
28
- function getProxyApiType(url) {
29
- const path = url.split("?")[0];
30
- return PROXY_API_TYPES[path] ?? null;
31
- }
32
23
  function logRejectedAuth(db, apiType, statusCode, errorMessage, request) {
33
24
  insertRequestLog(db, {
34
25
  id: randomUUID(),