llm-simple-router 1.0.22 → 1.0.23

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 (87) hide show
  1. package/dist/admin/providers.d.ts +1 -1
  2. package/dist/app/compose-container.d.ts +19 -0
  3. package/dist/app/compose-container.js +84 -0
  4. package/dist/app/create-app.d.ts +14 -0
  5. package/dist/app/create-app.js +148 -0
  6. package/dist/app/register-hooks.d.ts +20 -0
  7. package/dist/app/register-hooks.js +50 -0
  8. package/dist/app/register-routes.d.ts +33 -0
  9. package/dist/app/register-routes.js +111 -0
  10. package/dist/index.d.ts +2 -3
  11. package/dist/index.js +38 -349
  12. package/dist/proxy/handler/failover-loop.d.ts +2 -114
  13. package/dist/proxy/handler/failover-loop.js +7 -322
  14. package/dist/proxy/handler/iteration-setup.d.ts +63 -0
  15. package/dist/proxy/handler/iteration-setup.js +118 -0
  16. package/dist/proxy/handler/reject-helpers.d.ts +44 -0
  17. package/dist/proxy/handler/reject-helpers.js +37 -0
  18. package/dist/proxy/handler/resilience-processor.d.ts +63 -0
  19. package/dist/proxy/handler/resilience-processor.js +183 -0
  20. package/dist/proxy/hooks/builtin/allowed-models.js +1 -0
  21. package/dist/proxy/hooks/builtin/enhancement-preprocess.js +1 -0
  22. package/dist/proxy/hooks/builtin/error-logging.js +1 -0
  23. package/dist/proxy/hooks/builtin/overflow-redirect.js +1 -0
  24. package/dist/proxy/pipeline/pipeline.d.ts +8 -0
  25. package/dist/proxy/pipeline/pipeline.js +30 -1
  26. package/dist/proxy/pipeline/register-hooks.js +2 -5
  27. package/dist/proxy/pipeline/types.d.ts +2 -0
  28. package/dist/proxy/transform/stream-oa2ant.d.ts +9 -3
  29. package/dist/proxy/transform/stream-oa2ant.js +85 -110
  30. package/dist/proxy/transport/stream.js +0 -1
  31. package/frontend-dist/assets/{AuthLayout-BvYyVNay.js → AuthLayout-BEunPgsR.js} +1 -1
  32. package/frontend-dist/assets/{Card-CrAceTFb.js → Card-BZD0hj9B.js} +1 -1
  33. package/frontend-dist/assets/{CardContent-otK_vTWx.js → CardContent-BhTk5WpA.js} +1 -1
  34. package/frontend-dist/assets/{CardTitle-BEMKGuDs.js → CardTitle-C5Sl7jiK.js} +1 -1
  35. package/frontend-dist/assets/{CascadingModelSelect-TP3nDN4K.js → CascadingModelSelect-hgX6cLgB.js} +1 -1
  36. package/frontend-dist/assets/{Checkbox-KBtwJU8Q.js → Checkbox-Dqa3Cscn.js} +1 -1
  37. package/frontend-dist/assets/{CollapsibleContent-CK4MEdEk.js → CollapsibleContent-BUWvYwG3.js} +1 -1
  38. package/frontend-dist/assets/{CollapsibleTrigger-C1YuylWt.js → CollapsibleTrigger-C2c7iZUZ.js} +1 -1
  39. package/frontend-dist/assets/{ConcurrencyControl-naUxnJl6.js → ConcurrencyControl-uAEadZZk.js} +1 -1
  40. package/frontend-dist/assets/{Dashboard-CsMzPzyB.js → Dashboard-p9RT-L9D.js} +1 -1
  41. package/frontend-dist/assets/{Input-CISfShtH.js → Input-BPPXjMAP.js} +1 -1
  42. package/frontend-dist/assets/{Label-CpOxo1sn.js → Label-DzxhTdQ4.js} +1 -1
  43. package/frontend-dist/assets/{Login-DWIJrfGr.js → Login-DMqFEBL4.js} +1 -1
  44. package/frontend-dist/assets/{Logs-Do6F6xpO.js → Logs-DzJOxs3R.js} +1 -1
  45. package/frontend-dist/assets/{ModelMappings-BeCtJid-.js → ModelMappings-BuV0mMP9.js} +1 -1
  46. package/frontend-dist/assets/{Monitor-BkKdwtEO.js → Monitor-Dn2gx28M.js} +1 -1
  47. package/frontend-dist/assets/{Providers-DZDMM3Ly.js → Providers-B_QNQkvT.js} +1 -1
  48. package/frontend-dist/assets/{ProxyEnhancement-Bf9reTLr.js → ProxyEnhancement-Bh-y0c8S.js} +1 -1
  49. package/frontend-dist/assets/{QuickSetup-D05Pbvny.js → QuickSetup-D6qvOOGi.js} +1 -1
  50. package/frontend-dist/assets/{RetryRules-ZYSMISJv.js → RetryRules-CUwxpJGe.js} +1 -1
  51. package/frontend-dist/assets/{RouterKeys-DyCXVjSV.js → RouterKeys-Cxveb03s.js} +1 -1
  52. package/frontend-dist/assets/{RovingFocusItem-DyThG4Ao.js → RovingFocusItem-DYJ__XYN.js} +1 -1
  53. package/frontend-dist/assets/{Schedules-BMdkLY7a.js → Schedules-rnyJkUeV.js} +1 -1
  54. package/frontend-dist/assets/{Separator-Cge3lk25.js → Separator-b0DQ-jYT.js} +1 -1
  55. package/frontend-dist/assets/{Settings-CqA17rFr.js → Settings-CM41fZsT.js} +1 -1
  56. package/frontend-dist/assets/{Setup-C03USty0.js → Setup-Bya7jD8e.js} +1 -1
  57. package/frontend-dist/assets/{Skeleton-DBYFTs7B.js → Skeleton-pTsffE91.js} +1 -1
  58. package/frontend-dist/assets/{Switch-CLdGo10G.js → Switch-i--nDLfp.js} +1 -1
  59. package/frontend-dist/assets/{TableHeader-BmXchBD9.js → TableHeader-DRb-Gna_.js} +1 -1
  60. package/frontend-dist/assets/{TabsTrigger-URUZbzCb.js → TabsTrigger-DQaC-KDG.js} +1 -1
  61. package/frontend-dist/assets/{UnifiedRequestDialog-ZW5x63E9.js → UnifiedRequestDialog-rD2niZDM.js} +1 -1
  62. package/frontend-dist/assets/{VisuallyHiddenInput-Bi3I1T8I.js → VisuallyHiddenInput-DDQbEHjB.js} +1 -1
  63. package/frontend-dist/assets/arrow-down-CB0JOt4K.js +1 -0
  64. package/frontend-dist/assets/{badge-eqb4qgGp.js → badge-DJ9n6zhE.js} +1 -1
  65. package/frontend-dist/assets/{button-mHNAa2aF.js → button-B-HZXeEu.js} +2 -2
  66. package/frontend-dist/assets/chevron-right-DUq1dLXp.js +1 -0
  67. package/frontend-dist/assets/{dialog-BBMCB4Zc.js → dialog-Dw7DmJaC.js} +1 -1
  68. package/frontend-dist/assets/{image-DTqw64FB.js → image-Dubo8LPE.js} +1 -1
  69. package/frontend-dist/assets/{index-Ddg5hheZ.js → index-MCgniW8y.js} +2 -2
  70. package/frontend-dist/assets/{model-patches-Bru1xRNm.js → model-patches-BIHguJrc.js} +1 -1
  71. package/frontend-dist/assets/{pencil-B6uEYOlv.js → pencil-Dln-wIX0.js} +1 -1
  72. package/frontend-dist/assets/plus-DzV5Sswo.js +1 -0
  73. package/frontend-dist/assets/search-B715YxWt.js +1 -0
  74. package/frontend-dist/assets/{sparkles-DyakAPX_.js → sparkles-BzwS4M1v.js} +1 -1
  75. package/frontend-dist/assets/{transform-domain-BYHRTHa-.js → transform-domain-BFwsHvrY.js} +1 -1
  76. package/frontend-dist/assets/{trash-2-KJKHmeJP.js → trash-2-BO8qkg26.js} +1 -1
  77. package/frontend-dist/assets/{useClipboard-2juraaSb.js → useClipboard-qNtjFACf.js} +1 -1
  78. package/frontend-dist/assets/{useLogRetention-CYfHJhhs.js → useLogRetention-kW0AqtjJ.js} +1 -1
  79. package/frontend-dist/assets/{useProviderGroups-Cg-m09or.js → useProviderGroups-DQ49CXye.js} +1 -1
  80. package/frontend-dist/index.html +2 -2
  81. package/package.json +1 -1
  82. package/dist/proxy/pipeline/hook-registry.d.ts +0 -20
  83. package/dist/proxy/pipeline/hook-registry.js +0 -24
  84. package/frontend-dist/assets/arrow-down-BCM-l0Jl.js +0 -1
  85. package/frontend-dist/assets/chevron-right-D0wqOqtp.js +0 -1
  86. package/frontend-dist/assets/plus-B2f9w-px.js +0 -1
  87. package/frontend-dist/assets/search-CczkyuG-.js +0 -1
@@ -21,7 +21,7 @@ export declare function serializeProviders(db: Database.Database, providers: Pro
21
21
  }): {
22
22
  id: string;
23
23
  name: string;
24
- api_type: "anthropic" | "openai" | "openai-responses";
24
+ api_type: "openai" | "openai-responses" | "anthropic";
25
25
  base_url: string;
26
26
  upstream_path: string | null;
27
27
  api_key: string;
@@ -0,0 +1,19 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type Database from "better-sqlite3";
3
+ import { ServiceContainer } from "../core/container.js";
4
+ import { LogFileWriter } from "../storage/log-file-writer.js";
5
+ import type { Config } from "../config/index.js";
6
+ export interface ComposeContainerResult {
7
+ container: ServiceContainer;
8
+ logFileWriter: LogFileWriter | null;
9
+ logsDir: string;
10
+ isMemoryDb: boolean;
11
+ }
12
+ export interface ComposeContainerOptions {
13
+ config: Config;
14
+ }
15
+ /**
16
+ * 注册所有服务到 ServiceContainer。
17
+ * 返回 container 及 logFileWriter(close 时需要 flush)。
18
+ */
19
+ export declare function composeContainer(db: Database.Database, opts: ComposeContainerOptions, app: FastifyInstance): ComposeContainerResult;
@@ -0,0 +1,84 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ import { ServiceContainer, SERVICE_KEYS } from "../core/container.js";
5
+ import { RetryRuleMatcher } from "../proxy/orchestration/retry-rules.js";
6
+ import { PluginRegistry } from "../proxy/transform/plugin-registry.js";
7
+ import { FormatRegistry } from "../proxy/format/registry.js";
8
+ import { openaiAdapter } from "../proxy/format/adapters/openai.js";
9
+ import { anthropicAdapter } from "../proxy/format/adapters/anthropic.js";
10
+ import { responsesAdapter } from "../proxy/format/adapters/responses.js";
11
+ import { openaiToAnthropicConverter } from "../proxy/format/converters/openai-anthropic.js";
12
+ import { anthropicToOpenAIConverter } from "../proxy/format/converters/anthropic-openai.js";
13
+ import { openaiToResponsesConverter } from "../proxy/format/converters/openai-responses.js";
14
+ import { responsesToOpenAIConverter } from "../proxy/format/converters/responses-openai.js";
15
+ import { responsesToAnthropicConverter } from "../proxy/format/converters/responses-anthropic.js";
16
+ import { anthropicToResponsesConverter } from "../proxy/format/converters/anthropic-responses.js";
17
+ import { SemaphoreManager, AdaptiveController } from "../core/concurrency/index.js";
18
+ import { RequestTracker } from "../core/monitor/index.js";
19
+ import { UsageWindowTracker } from "../proxy/routing/usage-window-tracker.js";
20
+ import { SessionTracker, DEFAULT_LOOP_PREVENTION_CONFIG } from "../core/loop-prevention/index.js";
21
+ import { LogFileWriter } from "../storage/log-file-writer.js";
22
+ import { ProxyAgentFactory } from "../proxy/transport/proxy-agent.js";
23
+ import { getDetailLogEnabled } from "../db/settings.js";
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+ /**
27
+ * 注册所有服务到 ServiceContainer。
28
+ * 返回 container 及 logFileWriter(close 时需要 flush)。
29
+ */
30
+ export function composeContainer(db, opts, app) {
31
+ const { config } = opts;
32
+ const container = new ServiceContainer();
33
+ container.register(SERVICE_KEYS.db, () => db);
34
+ container.register(SERVICE_KEYS.matcher, (c) => {
35
+ const m = new RetryRuleMatcher();
36
+ m.load(c.resolve(SERVICE_KEYS.db));
37
+ return m;
38
+ });
39
+ container.register(SERVICE_KEYS.semaphoreManager, () => new SemaphoreManager());
40
+ container.register(SERVICE_KEYS.tracker, (c) => {
41
+ const t = new RequestTracker({ semaphoreManager: c.resolve(SERVICE_KEYS.semaphoreManager), logger: app.log });
42
+ t.startPushInterval();
43
+ return t;
44
+ });
45
+ container.register(SERVICE_KEYS.usageWindowTracker, (c) => {
46
+ const uwt = new UsageWindowTracker(c.resolve(SERVICE_KEYS.db));
47
+ uwt.reconcileOnStartup();
48
+ return uwt;
49
+ });
50
+ container.register(SERVICE_KEYS.sessionTracker, () => new SessionTracker(DEFAULT_LOOP_PREVENTION_CONFIG.sessionTracker));
51
+ // 文件日志写入器
52
+ const isMemoryDb = config.DB_PATH === ":memory:";
53
+ const logsDir = isMemoryDb ? "" : join(dirname(config.DB_PATH), "logs");
54
+ const logFileWriter = isMemoryDb
55
+ ? null
56
+ : new LogFileWriter(logsDir, { enabled: getDetailLogEnabled(db) });
57
+ container.register(SERVICE_KEYS.logFileWriter, () => logFileWriter);
58
+ // AdaptiveController(依赖已注册的 semaphoreManager)
59
+ container.register(SERVICE_KEYS.adaptiveController, (c) => {
60
+ const ac = new AdaptiveController(c.resolve(SERVICE_KEYS.semaphoreManager), app.log);
61
+ return ac;
62
+ });
63
+ // PluginRegistry
64
+ const pluginRegistry = new PluginRegistry();
65
+ pluginRegistry.loadFromDB(db);
66
+ const pluginsDir = path.resolve(__dirname, "../../plugins/transform");
67
+ pluginRegistry.scanPluginsDir(pluginsDir);
68
+ container.register(SERVICE_KEYS.pluginRegistry, () => pluginRegistry);
69
+ // FormatRegistry(3 adapters + 6 converters)
70
+ const formatRegistry = new FormatRegistry();
71
+ formatRegistry.registerAdapter(openaiAdapter);
72
+ formatRegistry.registerAdapter(anthropicAdapter);
73
+ formatRegistry.registerAdapter(responsesAdapter);
74
+ formatRegistry.registerConverter(openaiToAnthropicConverter);
75
+ formatRegistry.registerConverter(anthropicToOpenAIConverter);
76
+ formatRegistry.registerConverter(openaiToResponsesConverter);
77
+ formatRegistry.registerConverter(responsesToOpenAIConverter);
78
+ formatRegistry.registerConverter(responsesToAnthropicConverter);
79
+ formatRegistry.registerConverter(anthropicToResponsesConverter);
80
+ container.register(SERVICE_KEYS.formatRegistry, () => formatRegistry);
81
+ // ProxyAgentFactory
82
+ container.register(SERVICE_KEYS.proxyAgentFactory, () => new ProxyAgentFactory());
83
+ return { container, logFileWriter, logsDir, isMemoryDb };
84
+ }
@@ -0,0 +1,14 @@
1
+ import { type FastifyInstance } from "fastify";
2
+ import type { CheckerOptions } from "../upgrade/checker.js";
3
+ import type { Config } from "../config/index.js";
4
+ import type Database from "better-sqlite3";
5
+ export interface CreateAppOptions {
6
+ config: Config;
7
+ db: Database.Database;
8
+ upgradeCheckerOptions?: CheckerOptions;
9
+ }
10
+ /**
11
+ * 创建 Fastify 实例并注册全局 hooks(errorHandler、onSend 信封包装等)。
12
+ * 纯基础设施层,不涉及业务容器或路由。
13
+ */
14
+ export declare function createAppInstance(opts: CreateAppOptions): FastifyInstance;
@@ -0,0 +1,148 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { randomUUID } from "crypto";
4
+ import Fastify from "fastify";
5
+ import { insertRequestLog } from "../db/logs.js";
6
+ import { HTTP_INTERNAL_ERROR, getProxyApiType } from "../core/constants.js";
7
+ import { loadRecommendedConfig } from "../config/recommended.js";
8
+ import { startUpgradeChecker } from "../admin/upgrade.js";
9
+ import { isAdminApiResponse, statusToApiCode, apiError } from "../admin/api-response.js";
10
+ import { API_CODE } from "../admin/api-response.js";
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const KB = 1024;
14
+ const MB = KB * KB;
15
+ const MAX_BODY_SIZE_MB = 50;
16
+ /**
17
+ * 创建 Fastify 实例并注册全局 hooks(errorHandler、onSend 信封包装等)。
18
+ * 纯基础设施层,不涉及业务容器或路由。
19
+ */
20
+ export function createAppInstance(opts) {
21
+ const { config, db, upgradeCheckerOptions } = opts;
22
+ const isDev = process.env.NODE_ENV !== "production";
23
+ const app = Fastify({
24
+ bodyLimit: MAX_BODY_SIZE_MB * MB,
25
+ logger: {
26
+ level: config.LOG_LEVEL,
27
+ ...(isDev
28
+ ? {
29
+ transport: {
30
+ target: "pino-pretty",
31
+ options: {
32
+ translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
33
+ ignore: "pid,hostname",
34
+ },
35
+ },
36
+ }
37
+ : {}),
38
+ },
39
+ ajv: {
40
+ customOptions: {
41
+ messages: true,
42
+ },
43
+ },
44
+ });
45
+ app.setSchemaErrorFormatter((errors) => {
46
+ const message = errors
47
+ .map((e) => {
48
+ const field = e.instancePath ? e.instancePath.slice(1) : e.params?.missingProperty ?? "field";
49
+ return `${field} ${e.message}`;
50
+ })
51
+ .join("; ");
52
+ return new Error(message);
53
+ });
54
+ // 记录请求到达时间,供全局错误处理计算延迟
55
+ app.addHook("onRequest", (request, reply, done) => {
56
+ request.receivedAt = Date.now();
57
+ // 全局 EPIPE 防护
58
+ const sock = request.raw.socket;
59
+ const socketErrorHandler = (err) => {
60
+ if (err.code === "EPIPE" || err.code === "ECONNRESET") {
61
+ request.log.debug({ err }, "client socket error");
62
+ }
63
+ else {
64
+ request.log.warn({ err }, "unexpected socket error");
65
+ }
66
+ };
67
+ sock.on("error", socketErrorHandler);
68
+ const replyErrorHandler = (err) => {
69
+ const code = err.code;
70
+ if (code === "EPIPE") {
71
+ request.log.debug({ err }, "client disconnected (EPIPE)");
72
+ }
73
+ else {
74
+ request.log.warn({ err }, "response stream error");
75
+ }
76
+ };
77
+ reply.raw.on("error", replyErrorHandler);
78
+ reply.raw.on("close", () => {
79
+ sock.removeListener("error", socketErrorHandler);
80
+ reply.raw.removeListener("error", replyErrorHandler);
81
+ });
82
+ done();
83
+ });
84
+ // 统一错误处理
85
+ app.setErrorHandler((error, request, reply) => {
86
+ const fastifyError = error;
87
+ const status = fastifyError.statusCode ?? HTTP_INTERNAL_ERROR;
88
+ if (!isAdminApiResponse(request.url)) {
89
+ const proxyApiType = getProxyApiType(request.url);
90
+ if (proxyApiType) {
91
+ request.log.error({ statusCode: status, err: error }, `Proxy request error: ${fastifyError.message}`);
92
+ const body = request.body;
93
+ const receivedAt = request.receivedAt;
94
+ const latencyMs = receivedAt ? Date.now() - receivedAt : 0;
95
+ try {
96
+ insertRequestLog(db, {
97
+ id: randomUUID(),
98
+ api_type: proxyApiType,
99
+ model: body?.model || null,
100
+ provider_id: null,
101
+ status_code: status,
102
+ latency_ms: latencyMs,
103
+ is_stream: body?.stream === true ? 1 : 0,
104
+ error_message: fastifyError.message,
105
+ created_at: new Date().toISOString(),
106
+ client_request: JSON.stringify({ headers: request.headers, ...(body ? { body } : {}) }),
107
+ router_key_id: request.routerKey?.id ?? null,
108
+ });
109
+ }
110
+ catch (logErr) {
111
+ request.log.error({ err: logErr }, "Failed to log proxy error to request_logs");
112
+ }
113
+ }
114
+ return reply.code(status).send({ error: { message: fastifyError.message } });
115
+ }
116
+ const code = statusToApiCode(status);
117
+ return reply.code(status).send(apiError(code, fastifyError.message));
118
+ });
119
+ // onSend hook:自动包装 Admin API 成功响应为信封格式
120
+ app.addHook("onSend", async (request, reply, payload) => {
121
+ if (!isAdminApiResponse(request.url, reply.getHeader("content-type"))) {
122
+ return payload;
123
+ }
124
+ if (typeof payload === "string") {
125
+ try {
126
+ const parsed = JSON.parse(payload);
127
+ if (parsed !== null && typeof parsed === "object" && "code" in parsed)
128
+ return payload;
129
+ const wrapped = {
130
+ code: API_CODE.SUCCESS,
131
+ message: "ok",
132
+ data: parsed,
133
+ };
134
+ return JSON.stringify(wrapped);
135
+ }
136
+ catch {
137
+ return payload;
138
+ }
139
+ }
140
+ return payload;
141
+ });
142
+ loadRecommendedConfig(path.resolve(__dirname, "../../config"));
143
+ startUpgradeChecker({
144
+ ...upgradeCheckerOptions,
145
+ configDir: path.resolve(__dirname, "../../config"),
146
+ });
147
+ return app;
148
+ }
@@ -0,0 +1,20 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type Database from "better-sqlite3";
3
+ import { ServiceContainer } from "../core/container.js";
4
+ import type { StateRegistry } from "../core/registry.js";
5
+ import { RetryRuleMatcher } from "../proxy/orchestration/retry-rules.js";
6
+ import { SemaphoreManager } from "../core/concurrency/index.js";
7
+ import { AdaptiveController } from "../core/concurrency/index.js";
8
+ export interface RegisterHooksResult {
9
+ stateRegistry: StateRegistry;
10
+ }
11
+ export interface RegisterHooksDeps {
12
+ matcher: RetryRuleMatcher;
13
+ semaphoreManager: SemaphoreManager;
14
+ adaptiveController: AdaptiveController;
15
+ }
16
+ /**
17
+ * 注册 auth middleware、内置 hooks、proxy handlers,构建 StateRegistry。
18
+ * 返回 stateRegistry 供 admin 路由使用。
19
+ */
20
+ export declare function registerAppHooks(app: FastifyInstance, db: Database.Database, container: ServiceContainer, deps: RegisterHooksDeps): RegisterHooksResult;
@@ -0,0 +1,50 @@
1
+ import { authMiddleware } from "../middleware/auth.js";
2
+ import { createProxyHandler } from "../proxy/handler/create-proxy-handler.js";
3
+ import { registerBuiltinHooks } from "../proxy/pipeline/register-hooks.js";
4
+ import { proxyPipeline } from "../proxy/pipeline/pipeline.js";
5
+ import { clearEnhancementConfigCache } from "../proxy/routing/enhancement-config.js";
6
+ /**
7
+ * 注册 auth middleware、内置 hooks、proxy handlers,构建 StateRegistry。
8
+ * 返回 stateRegistry 供 admin 路由使用。
9
+ */
10
+ export function registerAppHooks(app, db, container, deps) {
11
+ const { matcher, semaphoreManager, adaptiveController } = deps;
12
+ app.register(authMiddleware, { db });
13
+ // 注册内置 hooks 到 ProxyPipeline(供 emit 执行 + Admin API 查询)
14
+ registerBuiltinHooks();
15
+ // --- Proxy handlers (Phase 3 pipeline) ---
16
+ const openaiHandler = createProxyHandler({
17
+ apiType: "openai",
18
+ paths: ["/v1/chat/completions", "/chat/completions"],
19
+ });
20
+ const anthropicHandler = createProxyHandler({
21
+ apiType: "anthropic",
22
+ paths: ["/v1/messages"],
23
+ });
24
+ const responsesHandler = createProxyHandler({
25
+ apiType: "openai-responses",
26
+ paths: ["/v1/responses", "/responses"],
27
+ });
28
+ app.register(openaiHandler, { db, container });
29
+ app.register(anthropicHandler, { db, container });
30
+ app.register(responsesHandler, { db, container });
31
+ // StateRegistry — Admin 层通过此接口触发 proxy 层状态刷新
32
+ const stateRegistry = {
33
+ refreshRetryRules: () => matcher.load(db),
34
+ updateProviderConcurrency: (providerId, cfg) => semaphoreManager.updateConfig(providerId, cfg),
35
+ removeProvider: (providerId) => semaphoreManager.remove(providerId),
36
+ removeAllProviders: () => semaphoreManager.removeAll(),
37
+ getProviderStatus: (providerId) => semaphoreManager.getStatus(providerId),
38
+ syncAdaptiveProvider: (providerId, cfg) => adaptiveController.syncProvider(providerId, cfg),
39
+ removeAdaptiveProvider: (providerId) => adaptiveController.remove(providerId),
40
+ getAdaptiveStatus: (providerId) => adaptiveController.getStatus(providerId),
41
+ reinitializeProviders: () => {
42
+ adaptiveController.removeAll();
43
+ // reinitializeProviders 的完整实现在 buildApp 层通过 initializeProviderState 完成
44
+ // StateRegistry 只重置 adaptive 状态,实际的重新初始化由调用方负责
45
+ },
46
+ clearEnhancementCache: () => clearEnhancementConfigCache(),
47
+ getPipelineHooks: () => proxyPipeline.getAllHooks(),
48
+ };
49
+ return { stateRegistry };
50
+ }
@@ -0,0 +1,33 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type Database from "better-sqlite3";
3
+ import { ServiceContainer } from "../core/container.js";
4
+ import { RequestTracker } from "../core/monitor/index.js";
5
+ import { AdaptiveController } from "../core/concurrency/index.js";
6
+ import { SemaphoreManager } from "../core/concurrency/index.js";
7
+ import { LogFileWriter } from "../storage/log-file-writer.js";
8
+ import { ProxyAgentFactory } from "../proxy/transport/proxy-agent.js";
9
+ import { ProxyConnectivityChecker } from "../proxy/transport/provider-connectivity.js";
10
+ import { PluginRegistry } from "../proxy/transform/plugin-registry.js";
11
+ import type { StateRegistry } from "../core/registry.js";
12
+ import type { Config } from "../config/index.js";
13
+ export interface RegisterRoutesOptions {
14
+ db: Database.Database;
15
+ config: Config;
16
+ container: ServiceContainer;
17
+ tracker: RequestTracker;
18
+ semaphoreManager: SemaphoreManager;
19
+ adaptiveController: AdaptiveController;
20
+ stateRegistry: StateRegistry;
21
+ logFileWriter: LogFileWriter | null;
22
+ logsDir: string;
23
+ isMemoryDb: boolean;
24
+ pluginRegistry: PluginRegistry;
25
+ proxyAgentFactory: ProxyAgentFactory;
26
+ connectivityChecker: ProxyConnectivityChecker;
27
+ initializeProviderStateFn: () => void;
28
+ }
29
+ /**
30
+ * 注册 admin routes、静态文件、/health 端点、定时任务,组装 close 函数。
31
+ * 返回 close 函数供 buildApp 消费。
32
+ */
33
+ export declare function registerRoutes(app: FastifyInstance, opts: RegisterRoutesOptions): () => Promise<void>;
@@ -0,0 +1,111 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { existsSync } from "node:fs";
4
+ import fastifyStatic from "@fastify/static";
5
+ import { SERVICE_KEYS } from "../core/container.js";
6
+ import { HTTP_NOT_FOUND } from "../core/constants.js";
7
+ import { adminRoutes } from "../admin/routes.js";
8
+ import { scheduleLogCleanup } from "../db/log-cleaner.js";
9
+ import { scheduleDbSizeMonitor } from "../db/db-size-monitor.js";
10
+ import { scheduleMetricsAggregator } from "../db/metrics-aggregator.js";
11
+ import { scheduleLogFileMaintenance } from "../storage/log-file-compressor.js";
12
+ import { stopUpgradeChecker } from "../admin/upgrade.js";
13
+ import { getLogFileRetentionDays } from "../db/settings.js";
14
+ /**
15
+ * 注册 admin routes、静态文件、/health 端点、定时任务,组装 close 函数。
16
+ * 返回 close 函数供 buildApp 消费。
17
+ */
18
+ export function registerRoutes(app, opts) {
19
+ const { db, config, container, tracker, semaphoreManager, logFileWriter, logsDir, isMemoryDb, proxyAgentFactory, stateRegistry, pluginRegistry, connectivityChecker, } = opts;
20
+ // Override reinitializeProviders to use the full implementation
21
+ const fullStateRegistry = {
22
+ ...stateRegistry,
23
+ reinitializeProviders: () => {
24
+ stateRegistry.reinitializeProviders();
25
+ opts.initializeProviderStateFn();
26
+ },
27
+ };
28
+ // Late-bound close ref — close 函数在 adminRoutes 注册之后才定义
29
+ const closeRef = { fn: async () => { } };
30
+ app.register(adminRoutes, {
31
+ db,
32
+ stateRegistry: fullStateRegistry,
33
+ tracker,
34
+ adaptiveController: opts.adaptiveController,
35
+ logFileWriter,
36
+ logsDir,
37
+ closeFn: () => closeRef.fn(),
38
+ pluginRegistry,
39
+ proxyAgentFactory,
40
+ connectivityChecker,
41
+ });
42
+ // 前端静态文件服务(生产环境)
43
+ const __filename = fileURLToPath(import.meta.url);
44
+ const frontendDist = path.resolve(process.env.FRONTEND_DIST || path.join(path.dirname(__filename), "../../frontend-dist"));
45
+ if (existsSync(frontendDist)) {
46
+ app.register(fastifyStatic, {
47
+ root: frontendDist,
48
+ prefix: "/admin/",
49
+ wildcard: false,
50
+ });
51
+ // SPA fallback
52
+ app.setNotFoundHandler((request, reply) => {
53
+ if ((request.url.startsWith("/admin/") || request.url === "/admin") &&
54
+ !request.url.startsWith("/admin/api")) {
55
+ return reply.sendFile("index.html");
56
+ }
57
+ reply.code(HTTP_NOT_FOUND).send({ error: { message: "Not Found" } });
58
+ });
59
+ }
60
+ else {
61
+ app.log.debug(`Frontend dist not found at ${frontendDist}, skipping static serving`);
62
+ }
63
+ app.get("/health", async () => {
64
+ return { status: "ok" };
65
+ });
66
+ const logCleanup = scheduleLogCleanup(db, app.log);
67
+ const metricsAggregator = scheduleMetricsAggregator(db, app.log);
68
+ const dbSizeMonitor = scheduleDbSizeMonitor(db, config.DB_PATH, { log: app.log });
69
+ let closed = false;
70
+ let close = async () => {
71
+ if (closed)
72
+ return;
73
+ closed = true;
74
+ stopUpgradeChecker();
75
+ logCleanup.stop();
76
+ metricsAggregator.stop();
77
+ dbSizeMonitor.stop();
78
+ tracker.stopPushInterval();
79
+ tracker.closeAllClients();
80
+ semaphoreManager.removeAll();
81
+ proxyAgentFactory.invalidateAll();
82
+ const sessionTracker = container.resolve(SERVICE_KEYS.sessionTracker);
83
+ sessionTracker.stop();
84
+ await logFileWriter?.stop();
85
+ const CLOSE_GRACE_PERIOD_MS = 2_000;
86
+ const forceClose = typeof app.server.closeAllConnections === "function"
87
+ ? setTimeout(() => app.server.closeAllConnections(), CLOSE_GRACE_PERIOD_MS)
88
+ : null;
89
+ if (forceClose)
90
+ forceClose.unref();
91
+ await app.close();
92
+ if (forceClose)
93
+ clearTimeout(forceClose);
94
+ db.close();
95
+ };
96
+ // 文件压缩和清理任务(仅非 :memory: 模式)
97
+ if (!isMemoryDb) {
98
+ const logFileMaintenance = scheduleLogFileMaintenance(logsDir, {
99
+ retentionDays: getLogFileRetentionDays(db),
100
+ log: app.log,
101
+ });
102
+ const prevClose = close;
103
+ close = async () => {
104
+ logFileMaintenance.stop();
105
+ await prevClose();
106
+ };
107
+ }
108
+ // 绑定到 late-bound ref(供 restart API 运行时调用)
109
+ closeRef.fn = close;
110
+ return close;
111
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { FastifyInstance } from "fastify";
3
- import { Config } from "./config/index.js";
2
+ import { type Config } from "./config/index.js";
4
3
  import { SemaphoreManager, AdaptiveController } from "./core/concurrency/index.js";
5
4
  import { RequestTracker } from "./core/monitor/index.js";
6
5
  import { UsageWindowTracker } from "./proxy/routing/usage-window-tracker.js";
@@ -17,7 +16,7 @@ export interface AppOptions {
17
16
  */
18
17
  export declare function initializeProviderState(db: Database.Database, semaphoreManager: SemaphoreManager, adaptiveController: AdaptiveController, tracker: RequestTracker): void;
19
18
  export declare function buildApp(options?: AppOptions): Promise<{
20
- app: FastifyInstance;
19
+ app: import("fastify").FastifyInstance;
21
20
  db: Database.Database;
22
21
  usageWindowTracker: UsageWindowTracker;
23
22
  tracker: RequestTracker;