llm-simple-router 0.5.0 → 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 (179) 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/routes.js +2 -0
  18. package/dist/admin/settings-import-export.js +3 -2
  19. package/dist/admin/settings.js +7 -5
  20. package/dist/admin/setup.js +3 -2
  21. package/dist/admin/stats.js +20 -3
  22. package/dist/admin/upgrade.d.ts +13 -0
  23. package/dist/admin/upgrade.js +114 -0
  24. package/dist/admin/usage.js +12 -24
  25. package/dist/config.d.ts +1 -1
  26. package/dist/config.js +1 -1
  27. package/dist/constants.d.ts +3 -0
  28. package/dist/constants.js +11 -0
  29. package/dist/db/index.d.ts +3 -3
  30. package/dist/db/index.js +2 -2
  31. package/dist/db/mappings.js +5 -8
  32. package/dist/db/metrics.js +3 -4
  33. package/dist/db/providers.d.ts +8 -0
  34. package/dist/db/providers.js +6 -0
  35. package/dist/db/retry-rules.d.ts +1 -0
  36. package/dist/db/retry-rules.js +3 -0
  37. package/dist/db/settings.d.ts +2 -0
  38. package/dist/db/settings.js +7 -0
  39. package/dist/db/stats.d.ts +1 -2
  40. package/dist/db/stats.js +7 -11
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +55 -34
  43. package/dist/metrics/metrics-extractor.js +1 -1
  44. package/dist/metrics/sse-parser.js +2 -0
  45. package/dist/middleware/admin-auth.js +6 -5
  46. package/dist/middleware/auth.js +1 -10
  47. package/dist/monitor/request-tracker.d.ts +1 -0
  48. package/dist/monitor/request-tracker.js +9 -45
  49. package/dist/monitor/runtime-collector.js +1 -1
  50. package/dist/monitor/stream-content-accumulator.d.ts +14 -0
  51. package/dist/monitor/stream-content-accumulator.js +58 -0
  52. package/dist/proxy/anthropic.d.ts +2 -1
  53. package/dist/proxy/anthropic.js +3 -3
  54. package/dist/proxy/enhancement/directive-parser.d.ts +18 -0
  55. package/dist/proxy/{directive-parser.js → enhancement/directive-parser.js} +44 -0
  56. package/dist/proxy/{enhancement-handler.js → enhancement/enhancement-handler.js} +152 -32
  57. package/dist/proxy/enhancement/index.d.ts +3 -0
  58. package/dist/proxy/enhancement/index.js +3 -0
  59. package/dist/proxy/{response-cleaner.js → enhancement/response-cleaner.js} +14 -0
  60. package/dist/proxy/log-helpers.d.ts +1 -1
  61. package/dist/proxy/mapping-resolver.js +4 -4
  62. package/dist/proxy/openai.d.ts +2 -1
  63. package/dist/proxy/openai.js +4 -4
  64. package/dist/proxy/orchestrator.d.ts +0 -1
  65. package/dist/proxy/orchestrator.js +1 -3
  66. package/dist/proxy/proxy-core.d.ts +0 -4
  67. package/dist/proxy/proxy-core.js +0 -2
  68. package/dist/proxy/proxy-handler.d.ts +1 -1
  69. package/dist/proxy/proxy-handler.js +52 -132
  70. package/dist/proxy/proxy-logging.d.ts +0 -2
  71. package/dist/proxy/proxy-logging.js +1 -3
  72. package/dist/proxy/resilience.d.ts +5 -2
  73. package/dist/proxy/resilience.js +16 -7
  74. package/dist/proxy/strategy/failover.js +2 -7
  75. package/dist/proxy/strategy/random.js +2 -2
  76. package/dist/proxy/strategy/round-robin.js +2 -2
  77. package/dist/proxy/strategy/scheduled.js +1 -8
  78. package/dist/proxy/strategy/targets-rule.d.ts +1 -0
  79. package/dist/proxy/strategy/targets-rule.js +5 -0
  80. package/dist/proxy/transport-fn.d.ts +25 -0
  81. package/dist/proxy/transport-fn.js +55 -0
  82. package/dist/proxy/transport.d.ts +0 -25
  83. package/dist/proxy/transport.js +0 -38
  84. package/dist/upgrade/checker.d.ts +25 -0
  85. package/dist/upgrade/checker.js +120 -0
  86. package/dist/upgrade/deployment.d.ts +2 -0
  87. package/dist/upgrade/deployment.js +20 -0
  88. package/dist/upgrade/version.d.ts +1 -0
  89. package/dist/upgrade/version.js +13 -0
  90. package/dist/utils/password.js +4 -2
  91. package/dist/utils/time-range.d.ts +9 -0
  92. package/dist/utils/time-range.js +40 -0
  93. package/frontend-dist/assets/CardContent-WrBnGhTg.js +1 -0
  94. package/frontend-dist/assets/CardTitle-BcDYk7cq.js +1 -0
  95. package/frontend-dist/assets/Checkbox-MZf0YsDG.js +1 -0
  96. package/frontend-dist/assets/CollapsibleTrigger-CrOH9HlW.js +1 -0
  97. package/frontend-dist/assets/Collection-DcTx_Y54.js +1 -0
  98. package/frontend-dist/assets/Dashboard-D0oDrSLr.js +3 -0
  99. package/frontend-dist/assets/DialogTitle-Cl5Cd7QH.js +1 -0
  100. package/frontend-dist/assets/{Input-l5ZurXX5.js → Input-O0ebU-Va.js} +1 -1
  101. package/frontend-dist/assets/Label-C_S0y7Um.js +1 -0
  102. package/frontend-dist/assets/Login-DGY7uF8P.js +1 -0
  103. package/frontend-dist/assets/Logs-ls8pv89b.js +1 -0
  104. package/frontend-dist/assets/ModelMappings-DGlf0S4s.js +1 -0
  105. package/frontend-dist/assets/Monitor-BSI87grz.js +1 -0
  106. package/frontend-dist/assets/PopperContent-C6Q7hDmf.js +1 -0
  107. package/frontend-dist/assets/Providers-ZkRpj8_m.js +1 -0
  108. package/frontend-dist/assets/ProxyEnhancement-DFPI1W6Z.js +5 -0
  109. package/frontend-dist/assets/RetryRules-DtM31qsl.js +1 -0
  110. package/frontend-dist/assets/RouterKeys-D63tRFKm.js +1 -0
  111. package/frontend-dist/assets/RovingFocusItem-BJoylAKU.js +1 -0
  112. package/frontend-dist/assets/SelectValue-CLp5z6_I.js +1 -0
  113. package/frontend-dist/assets/Settings-DSgRKbTQ.js +6 -0
  114. package/frontend-dist/assets/Setup-BDmj6CRk.js +1 -0
  115. package/frontend-dist/assets/Switch-Wz-t_zkv.js +1 -0
  116. package/frontend-dist/assets/TableHeader-DGtcqGkw.js +1 -0
  117. package/frontend-dist/assets/TabsTrigger-CPCi2HIa.js +1 -0
  118. package/frontend-dist/assets/Teleport-DdjYHlNK.js +3 -0
  119. package/frontend-dist/assets/TooltipTrigger-H_QoPY1n.js +1 -0
  120. package/frontend-dist/assets/UnifiedRequestDialog-BAAfMJJl.js +3 -0
  121. package/frontend-dist/assets/{VisuallyHidden-BwwTtzb9.js → VisuallyHidden-Cyk-jWwh.js} +1 -1
  122. package/frontend-dist/assets/VisuallyHiddenInput-CYjNe_H8.js +1 -0
  123. package/frontend-dist/assets/alert-dialog-Bi3dliLl.js +1 -0
  124. package/frontend-dist/assets/badge-Kkta3e9W.js +1 -0
  125. package/frontend-dist/assets/button-BQ3s7yNh.js +12 -0
  126. package/frontend-dist/assets/{createLucideIcon-Biq59l_W.js → createLucideIcon-D1tkPDOQ.js} +1 -1
  127. package/frontend-dist/assets/dialog-DoIATUYw.js +1 -0
  128. package/frontend-dist/assets/{file-text-DoRW0hQW.js → file-text-Dt6QP1bZ.js} +1 -1
  129. package/frontend-dist/assets/index-BY0E7CHR.js +1 -0
  130. package/frontend-dist/assets/index-Bnrh1mFY.css +1 -0
  131. package/frontend-dist/assets/lib-CxwxnlwW.js +1 -0
  132. package/frontend-dist/assets/{ohash.D__AXeF1-BGxYMs6k.js → ohash.D__AXeF1-b0PiKZB_.js} +1 -1
  133. package/frontend-dist/assets/{useClipboard-vaHkvJHw.js → useClipboard-Cnnz6AAN.js} +1 -1
  134. package/frontend-dist/assets/useLogRetention-DYP5LOAc.js +1 -0
  135. package/frontend-dist/assets/useNonce-DKbOCfgM.js +1 -0
  136. package/frontend-dist/assets/x-CAoitXRt.js +1 -0
  137. package/frontend-dist/index.html +18 -9
  138. package/package.json +2 -1
  139. package/dist/proxy/directive-parser.d.ts +0 -7
  140. package/frontend-dist/assets/CardContent-CIO85eT6.js +0 -1
  141. package/frontend-dist/assets/CardTitle-DiqIReMT.js +0 -1
  142. package/frontend-dist/assets/Checkbox-C2u5pIp4.js +0 -1
  143. package/frontend-dist/assets/CollapsibleTrigger-RKFL41om.js +0 -1
  144. package/frontend-dist/assets/Collection-iiNnuTQj.js +0 -1
  145. package/frontend-dist/assets/Dashboard-DOEqP6gF.js +0 -3
  146. package/frontend-dist/assets/DialogTitle-CEqndrf6.js +0 -1
  147. package/frontend-dist/assets/Label-PgGtS8v2.js +0 -1
  148. package/frontend-dist/assets/Login-DaN6ZcCx.js +0 -1
  149. package/frontend-dist/assets/Logs-CleRQ7Xk.js +0 -1
  150. package/frontend-dist/assets/ModelMappings-CacA_ua_.js +0 -1
  151. package/frontend-dist/assets/Monitor-LSMFOBN2.js +0 -1
  152. package/frontend-dist/assets/PopperContent-zLFHqQP0.js +0 -1
  153. package/frontend-dist/assets/Providers-NT5MUDU0.js +0 -1
  154. package/frontend-dist/assets/ProxyEnhancement-DhOy8nNy.js +0 -5
  155. package/frontend-dist/assets/RetryRules-7arWa3jB.js +0 -1
  156. package/frontend-dist/assets/RouterKeys-CdaZunRg.js +0 -1
  157. package/frontend-dist/assets/SelectValue-CSg-MKW_.js +0 -1
  158. package/frontend-dist/assets/Settings-1ntV9XE3.js +0 -6
  159. package/frontend-dist/assets/Setup-CXLTDhYJ.js +0 -1
  160. package/frontend-dist/assets/Switch-DivrIFE3.js +0 -1
  161. package/frontend-dist/assets/TableHeader-Bn0bodWx.js +0 -1
  162. package/frontend-dist/assets/TabsContent-MWvOH_LJ.js +0 -1
  163. package/frontend-dist/assets/TabsTrigger-WKkUfO2M.js +0 -1
  164. package/frontend-dist/assets/Teleport-B0PNXZbP.js +0 -3
  165. package/frontend-dist/assets/UnifiedRequestDialog-Ba2e7YuJ.js +0 -3
  166. package/frontend-dist/assets/VisuallyHiddenInput-EGZSP7s8.js +0 -1
  167. package/frontend-dist/assets/alert-dialog-CS1yFhdV.js +0 -1
  168. package/frontend-dist/assets/badge-C-QcC5n2.js +0 -1
  169. package/frontend-dist/assets/button-Dbz2Be22.js +0 -12
  170. package/frontend-dist/assets/dialog-Cr0YQlLW.js +0 -1
  171. package/frontend-dist/assets/index-0H2uCGbx.js +0 -1
  172. package/frontend-dist/assets/index-D-cdVNCb.css +0 -1
  173. package/frontend-dist/assets/lib-B0lieqgg.js +0 -1
  174. package/frontend-dist/assets/useForwardExpose-C2_ks3sW.js +0 -1
  175. package/frontend-dist/assets/useLogRetention-Cs_fiKql.js +0 -1
  176. package/frontend-dist/assets/useNonce-C9do0jOI.js +0 -1
  177. package/frontend-dist/assets/x-BlTnH_0_.js +0 -1
  178. /package/dist/proxy/{enhancement-handler.d.ts → enhancement/enhancement-handler.d.ts} +0 -0
  179. /package/dist/proxy/{response-cleaner.d.ts → enhancement/response-cleaner.d.ts} +0 -0
@@ -1,14 +1,16 @@
1
1
  import { getLogRetentionDays, setLogRetentionDays, getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, getSetting, } from "../db/settings.js";
2
+ import { HTTP_BAD_REQUEST } from "./constants.js";
3
+ import { API_CODE, apiError } from "./api-response.js";
2
4
  export const adminSettingsRoutes = (app, options, done) => {
3
5
  const { db } = options;
4
6
  app.get("/admin/api/settings/log-retention", async () => {
5
7
  return { days: getLogRetentionDays(db) };
6
8
  });
7
- app.put("/admin/api/settings/log-retention", async (request) => {
9
+ app.put("/admin/api/settings/log-retention", async (request, reply) => {
8
10
  const { days } = request.body;
9
11
  const MAX_LOG_RETENTION_DAYS = 90;
10
12
  if (!Number.isInteger(days) || days < 0 || days > MAX_LOG_RETENTION_DAYS) {
11
- throw { statusCode: 400, message: "days must be integer 0-90" };
13
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "days must be integer 0-90"));
12
14
  }
13
15
  setLogRetentionDays(db, days);
14
16
  return { days };
@@ -31,17 +33,17 @@ export const adminSettingsRoutes = (app, options, done) => {
31
33
  },
32
34
  };
33
35
  });
34
- app.put("/admin/api/settings/db-size-thresholds", async (request) => {
36
+ app.put("/admin/api/settings/db-size-thresholds", async (request, reply) => {
35
37
  const body = request.body;
36
38
  if (body.dbMaxSizeMb !== undefined) {
37
39
  if (!Number.isFinite(body.dbMaxSizeMb) || body.dbMaxSizeMb < 1) {
38
- throw { statusCode: 400, message: "dbMaxSizeMb must be a positive number" };
40
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "dbMaxSizeMb must be a positive number"));
39
41
  }
40
42
  setDbMaxSizeMb(db, Math.round(body.dbMaxSizeMb));
41
43
  }
42
44
  if (body.logTableMaxSizeMb !== undefined) {
43
45
  if (!Number.isFinite(body.logTableMaxSizeMb) || body.logTableMaxSizeMb < 1) {
44
- throw { statusCode: 400, message: "logTableMaxSizeMb must be a positive number" };
46
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "logTableMaxSizeMb must be a positive number"));
45
47
  }
46
48
  setLogTableMaxSizeMb(db, Math.round(body.logTableMaxSizeMb));
47
49
  }
@@ -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();
@@ -0,0 +1,13 @@
1
+ import { FastifyPluginCallback } from 'fastify';
2
+ import Database from 'better-sqlite3';
3
+ import { CheckerOptions } from '../upgrade/checker.js';
4
+ interface UpgradeRoutesOptions {
5
+ db: Database.Database;
6
+ }
7
+ export declare function startUpgradeChecker(opts?: CheckerOptions): {
8
+ check: (sourceOverride?: string) => Promise<void>;
9
+ getStatus: () => import("../upgrade/checker.js").UpgradeStatus;
10
+ };
11
+ export declare function stopUpgradeChecker(): void;
12
+ export declare const adminUpgradeRoutes: FastifyPluginCallback<UpgradeRoutesOptions>;
13
+ export {};
@@ -0,0 +1,114 @@
1
+ import { getConfigSyncSource, setConfigSyncSource } from '../db/settings.js';
2
+ import { detectDeployment } from '../upgrade/deployment.js';
3
+ import { createUpgradeChecker, fetchJson } from '../upgrade/checker.js';
4
+ import { reloadConfig } from '../config/recommended.js';
5
+ import { execSync } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { HTTP_BAD_REQUEST, HTTP_INTERNAL_ERROR } from '../constants.js';
9
+ import { API_CODE, apiError } from './api-response.js';
10
+ const GITHUB_CONFIG_BASE = 'https://raw.githubusercontent.com/zhushanwen321/llm-simple-router/main/config';
11
+ const GITEE_CONFIG_BASE = 'https://gitee.com/zzzzswszzzz/llm-simple-router/raw/main/config';
12
+ const CHECK_INTERVAL_MS = 60 * 60 * 1000; // eslint-disable-line no-magic-numbers
13
+ const JSON_INDENT = 2;
14
+ // 模块级单例:checker 和定时器
15
+ let checker = null;
16
+ let intervalId = null;
17
+ export function startUpgradeChecker(opts) {
18
+ if (checker)
19
+ return checker;
20
+ checker = createUpgradeChecker(opts);
21
+ // 启动时检查一次,之后每小时
22
+ checker.check();
23
+ intervalId = setInterval(() => checker.check(), CHECK_INTERVAL_MS);
24
+ return checker;
25
+ }
26
+ export function stopUpgradeChecker() {
27
+ if (intervalId)
28
+ clearInterval(intervalId);
29
+ checker = null;
30
+ intervalId = null;
31
+ }
32
+ function getConfigBaseUrl(source) {
33
+ return source === 'gitee' ? GITEE_CONFIG_BASE : GITHUB_CONFIG_BASE;
34
+ }
35
+ export const adminUpgradeRoutes = (app, options, done) => {
36
+ const { db } = options;
37
+ app.get('/admin/api/upgrade/status', async (_req, reply) => {
38
+ const c = checker ?? createUpgradeChecker();
39
+ const deployment = detectDeployment();
40
+ const syncSource = getConfigSyncSource(db);
41
+ return reply.send({ ...c.getStatus(), deployment, syncSource });
42
+ });
43
+ app.post('/admin/api/upgrade/check', async (_req, reply) => {
44
+ const c = checker ?? createUpgradeChecker();
45
+ const syncSource = getConfigSyncSource(db);
46
+ await c.check(getConfigBaseUrl(syncSource));
47
+ return reply.send({ ok: true });
48
+ });
49
+ app.put('/admin/api/upgrade/sync-source', async (req, reply) => {
50
+ const { source } = req.body;
51
+ if (source !== 'github' && source !== 'gitee') {
52
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, 'source must be github or gitee'));
53
+ }
54
+ setConfigSyncSource(db, source);
55
+ return reply.send({ ok: true });
56
+ });
57
+ app.post('/admin/api/upgrade/execute', async (req, reply) => {
58
+ const deployment = detectDeployment();
59
+ if (deployment !== 'npm') {
60
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, '仅支持 npm 全局安装模式下自动升级'));
61
+ }
62
+ const { version } = req.body;
63
+ if (!version) {
64
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, 'version is required'));
65
+ }
66
+ if (!/^\d+\.\d+\.\d+$/.test(version)) {
67
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, '无效版本号格式'));
68
+ }
69
+ try {
70
+ execSync(`npm install -g llm-simple-router@${version}`, {
71
+ stdio: 'pipe',
72
+ timeout: 120_000,
73
+ });
74
+ return reply.send({ ok: true, version });
75
+ }
76
+ catch (err) {
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `升级失败: ${msg}`));
79
+ }
80
+ });
81
+ app.post('/admin/api/upgrade/sync-config', async (req, reply) => {
82
+ const { source } = req.body;
83
+ if (source !== 'github' && source !== 'gitee') {
84
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, 'source must be github or gitee'));
85
+ }
86
+ const base = getConfigBaseUrl(source);
87
+ const configDir = path.resolve(process.cwd(), 'config');
88
+ try {
89
+ fs.mkdirSync(configDir, { recursive: true });
90
+ const [providersResult, rulesResult] = await Promise.allSettled([
91
+ fetchJson(`${base}/recommended-providers.json`),
92
+ fetchJson(`${base}/recommended-retry-rules.json`),
93
+ ]);
94
+ if (providersResult.status === 'fulfilled') {
95
+ fs.writeFileSync(path.join(configDir, 'recommended-providers.json'), JSON.stringify(providersResult.value, null, JSON_INDENT));
96
+ }
97
+ if (rulesResult.status === 'fulfilled') {
98
+ fs.writeFileSync(path.join(configDir, 'recommended-retry-rules.json'), JSON.stringify(rulesResult.value, null, JSON_INDENT));
99
+ }
100
+ if (providersResult.status === 'rejected' && rulesResult.status === 'rejected') {
101
+ throw new Error('同步失败: 无法获取 providers 和 retry-rules 配置');
102
+ }
103
+ reloadConfig();
104
+ if (checker)
105
+ await checker.check(getConfigBaseUrl(source));
106
+ return reply.send({ ok: true });
107
+ }
108
+ catch (err) {
109
+ const msg = err instanceof Error ? err.message : String(err);
110
+ return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `同步失败: ${msg}`));
111
+ }
112
+ });
113
+ done();
114
+ };
@@ -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
+ }
@@ -8,3 +8,5 @@ export declare function getDbMaxSizeMb(db: Database.Database): number;
8
8
  export declare function setDbMaxSizeMb(db: Database.Database, mb: number): void;
9
9
  export declare function getLogTableMaxSizeMb(db: Database.Database): number;
10
10
  export declare function setLogTableMaxSizeMb(db: Database.Database, mb: number): void;
11
+ export declare function getConfigSyncSource(db: Database.Database): "github" | "gitee";
12
+ export declare function setConfigSyncSource(db: Database.Database, source: "github" | "gitee"): void;
@@ -32,3 +32,10 @@ export function getLogTableMaxSizeMb(db) {
32
32
  export function setLogTableMaxSizeMb(db, mb) {
33
33
  setSetting(db, "log_table_max_size_mb", String(mb));
34
34
  }
35
+ export function getConfigSyncSource(db) {
36
+ const val = getSetting(db, "config_sync_source");
37
+ return val === "gitee" ? "gitee" : "github";
38
+ }
39
+ export function setConfigSyncSource(db, source) {
40
+ setSetting(db, "config_sync_source", source);
41
+ }
@@ -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.d.ts CHANGED
@@ -2,10 +2,12 @@
2
2
  import { FastifyInstance } from "fastify";
3
3
  import { Config } from "./config.js";
4
4
  import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
5
+ import { CheckerOptions } from "./upgrade/checker.js";
5
6
  import Database from "better-sqlite3";
6
7
  export interface AppOptions {
7
8
  config?: Config;
8
9
  db?: Database.Database;
10
+ upgradeCheckerOptions?: CheckerOptions;
9
11
  }
10
12
  export declare function buildApp(options?: AppOptions): Promise<{
11
13
  app: FastifyInstance;