llm-simple-router 0.9.33 → 0.10.0

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 (136) hide show
  1. package/dist/admin/providers.js +1 -1
  2. package/dist/admin/routes.js +5 -0
  3. package/dist/admin/schedules.js +1 -1
  4. package/dist/admin/upgrade.js +3 -3
  5. package/dist/config/recommended.js +2 -2
  6. package/dist/core/constants.d.ts +1 -0
  7. package/dist/core/constants.js +1 -0
  8. package/dist/core/container.d.ts +1 -0
  9. package/dist/core/container.js +1 -0
  10. package/dist/db/db-size-monitor.js +1 -1
  11. package/dist/db/log-cleaner.js +1 -1
  12. package/dist/db/settings.js +3 -3
  13. package/dist/index.js +44 -8
  14. package/dist/proxy/format/adapters/anthropic.d.ts +2 -0
  15. package/dist/proxy/format/adapters/anthropic.js +18 -0
  16. package/dist/proxy/format/adapters/openai.d.ts +2 -0
  17. package/dist/proxy/format/adapters/openai.js +14 -0
  18. package/dist/proxy/format/adapters/responses.d.ts +2 -0
  19. package/dist/proxy/format/adapters/responses.js +9 -0
  20. package/dist/proxy/format/adapters/shared-error-meta.d.ts +9 -0
  21. package/dist/proxy/format/adapters/shared-error-meta.js +14 -0
  22. package/dist/proxy/format/converters/anthropic-openai.d.ts +1 -0
  23. package/dist/proxy/format/converters/anthropic-openai.js +11 -0
  24. package/dist/proxy/format/converters/anthropic-responses.d.ts +1 -0
  25. package/dist/proxy/format/converters/anthropic-responses.js +11 -0
  26. package/dist/proxy/format/converters/openai-anthropic.d.ts +1 -0
  27. package/dist/proxy/format/converters/openai-anthropic.js +11 -0
  28. package/dist/proxy/format/converters/openai-responses.d.ts +1 -0
  29. package/dist/proxy/format/converters/openai-responses.js +11 -0
  30. package/dist/proxy/format/converters/responses-anthropic.d.ts +1 -0
  31. package/dist/proxy/format/converters/responses-anthropic.js +11 -0
  32. package/dist/proxy/format/converters/responses-openai.d.ts +1 -0
  33. package/dist/proxy/format/converters/responses-openai.js +11 -0
  34. package/dist/proxy/format/registry.d.ts +17 -0
  35. package/dist/proxy/format/registry.js +50 -0
  36. package/dist/proxy/format/types.d.ts +27 -0
  37. package/dist/proxy/format/types.js +16 -0
  38. package/dist/proxy/handler/create-proxy-handler.d.ts +14 -0
  39. package/dist/proxy/handler/create-proxy-handler.js +235 -0
  40. package/dist/proxy/handler/failover-loop.d.ts +19 -0
  41. package/dist/proxy/handler/failover-loop.js +407 -0
  42. package/dist/proxy/handler/proxy-handler-utils.js +1 -1
  43. package/dist/proxy/hooks/builtin/allowed-models.d.ts +12 -0
  44. package/dist/proxy/hooks/builtin/allowed-models.js +37 -0
  45. package/dist/proxy/hooks/builtin/enhancement-preprocess.d.ts +2 -0
  46. package/dist/proxy/hooks/builtin/enhancement-preprocess.js +84 -0
  47. package/dist/proxy/hooks/builtin/error-logging.d.ts +2 -0
  48. package/dist/proxy/hooks/builtin/error-logging.js +86 -0
  49. package/dist/proxy/hooks/builtin/overflow-redirect.d.ts +2 -0
  50. package/dist/proxy/hooks/builtin/overflow-redirect.js +39 -0
  51. package/dist/proxy/hooks/builtin/plugin-request.d.ts +2 -0
  52. package/dist/proxy/hooks/builtin/plugin-request.js +49 -0
  53. package/dist/proxy/hooks/builtin/provider-patches.d.ts +2 -0
  54. package/dist/proxy/hooks/builtin/provider-patches.js +36 -0
  55. package/dist/proxy/hooks/builtin/request-logging.d.ts +2 -0
  56. package/dist/proxy/hooks/builtin/request-logging.js +72 -0
  57. package/dist/proxy/hooks/plugin-bridge.d.ts +7 -0
  58. package/dist/proxy/hooks/plugin-bridge.js +106 -0
  59. package/dist/proxy/hooks/sse-event-transform.d.ts +13 -0
  60. package/dist/proxy/hooks/sse-event-transform.js +59 -0
  61. package/dist/proxy/orchestration/resilience.js +2 -3
  62. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +1 -1
  63. package/dist/proxy/patch/deepseek/utils.js +1 -1
  64. package/dist/proxy/pipeline/context.d.ts +3 -0
  65. package/dist/proxy/pipeline/context.js +31 -0
  66. package/dist/proxy/pipeline/hook-registry.d.ts +20 -0
  67. package/dist/proxy/pipeline/hook-registry.js +24 -0
  68. package/dist/proxy/pipeline/pipeline.d.ts +13 -0
  69. package/dist/proxy/pipeline/pipeline.js +26 -0
  70. package/dist/proxy/pipeline/register-hooks.d.ts +1 -0
  71. package/dist/proxy/pipeline/register-hooks.js +23 -0
  72. package/dist/proxy/pipeline/types.d.ts +63 -0
  73. package/dist/proxy/pipeline/types.js +10 -0
  74. package/dist/proxy/proxy-core.js +1 -1
  75. package/dist/proxy/transform/message-mapper.js +8 -8
  76. package/dist/proxy/transform/plugin-types.d.ts +47 -17
  77. package/dist/proxy/transform/request-bridge-responses.js +21 -21
  78. package/dist/proxy/transform/request-transform-responses.js +24 -24
  79. package/dist/proxy/transform/request-transform.js +1 -3
  80. package/dist/proxy/transform/response-bridge-responses.js +13 -13
  81. package/dist/proxy/transform/response-transform-responses.js +10 -10
  82. package/dist/proxy/transform/sanitize.js +5 -1
  83. package/dist/proxy/transform/stream-transform-base.js +2 -2
  84. package/dist/proxy/transport/transport-fn.js +0 -1
  85. package/frontend-dist/assets/{CardContent-CPeHI_vO.js → CardContent-eu5DgvQn.js} +1 -1
  86. package/frontend-dist/assets/{CardTitle-DiF3FpGs.js → CardTitle-B8YCOfF5.js} +1 -1
  87. package/frontend-dist/assets/{Checkbox-BpQYgA_C.js → Checkbox-BMzIHy1r.js} +1 -1
  88. package/frontend-dist/assets/{CollapsibleContent-CUb_IZ--.js → CollapsibleContent-C_nFgqIi.js} +1 -1
  89. package/frontend-dist/assets/{CollapsibleTrigger-DSh5Vnmj.js → CollapsibleTrigger-CPqhHT7X.js} +1 -1
  90. package/frontend-dist/assets/{Dashboard-DMaq-T6I.js → Dashboard-BLxKSSXs.js} +1 -1
  91. package/frontend-dist/assets/{Input-C9E7es5X.js → Input-DkWAdwBI.js} +1 -1
  92. package/frontend-dist/assets/{Label-kvNNc1bh.js → Label-CJ2IIzlx.js} +1 -1
  93. package/frontend-dist/assets/Login-Dw4f6v19.js +1 -0
  94. package/frontend-dist/assets/{Logs-zr32VuD0.js → Logs-Df503HB_.js} +1 -1
  95. package/frontend-dist/assets/{MappingEntryEditor-D6ABslWN.js → MappingEntryEditor-Dg6zjLjk.js} +1 -1
  96. package/frontend-dist/assets/{ModelCard-Dn_8Lnr-.js → ModelCard-Ct1-_CKo.js} +1 -1
  97. package/frontend-dist/assets/ModelMappings-QsbYO-BL.js +1 -0
  98. package/frontend-dist/assets/{Monitor-CnQrBEqa.js → Monitor-DIiuMy53.js} +1 -1
  99. package/frontend-dist/assets/{Providers-CK4mOZS5.js → Providers-NyDmz-Aa.js} +1 -1
  100. package/frontend-dist/assets/{ProxyEnhancement-DQIrpH8f.js → ProxyEnhancement-DNtlZsDS.js} +1 -1
  101. package/frontend-dist/assets/QuickSetup-Ce3GmdB-.js +1 -0
  102. package/frontend-dist/assets/{RetryRules-Botm2DuB.js → RetryRules-EqbyA2z0.js} +1 -1
  103. package/frontend-dist/assets/{RouterKeys-C9ZluPMi.js → RouterKeys-DMKJn6FW.js} +1 -1
  104. package/frontend-dist/assets/{RovingFocusItem-B4cr2rFZ.js → RovingFocusItem-CebAv0Zc.js} +1 -1
  105. package/frontend-dist/assets/Schedules-DxHuzar_.js +1 -0
  106. package/frontend-dist/assets/{Settings-Dd9LXNyo.js → Settings-DwULwXPR.js} +2 -2
  107. package/frontend-dist/assets/{Setup-DXTFJRxq.js → Setup-DNhGIF-c.js} +1 -1
  108. package/frontend-dist/assets/{Switch-CkJ3KppU.js → Switch-ChIi3Y2O.js} +1 -1
  109. package/frontend-dist/assets/{TooltipTrigger-DtY0_zHb.js → TooltipTrigger-BcK9IvPW.js} +1 -1
  110. package/frontend-dist/assets/{TransformRulesForm-DpFk5xx0.js → TransformRulesForm-CXJhbKc5.js} +1 -1
  111. package/frontend-dist/assets/{UnifiedRequestDialog-Cpn9RNu1.js → UnifiedRequestDialog-I_J8h6X5.js} +1 -1
  112. package/frontend-dist/assets/{VisuallyHiddenInput-DvHAsFOO.js → VisuallyHiddenInput-awNWLd76.js} +1 -1
  113. package/frontend-dist/assets/{button-CjKaq4D4.js → button-C50lT23d.js} +2 -2
  114. package/frontend-dist/assets/{copy-BYleSlPg.js → copy-BHunr2eT.js} +1 -1
  115. package/frontend-dist/assets/{dialog-DlHPBQX7.js → dialog-Bts2bGWX.js} +1 -1
  116. package/frontend-dist/assets/{index-C19mbJF3.js → index-CGNAzHFe.js} +2 -2
  117. package/frontend-dist/assets/{trash-2-C6EEjS9z.js → trash-2-CkUDjZuE.js} +1 -1
  118. package/frontend-dist/assets/{useClipboard-C_hBlc9t.js → useClipboard-C3rvTuuE.js} +1 -1
  119. package/frontend-dist/assets/useLogRetention-DL5cOZfI.js +1 -0
  120. package/frontend-dist/index.html +2 -2
  121. package/package.json +1 -1
  122. package/dist/proxy/handler/anthropic.d.ts +0 -7
  123. package/dist/proxy/handler/anthropic.js +0 -43
  124. package/dist/proxy/handler/openai.d.ts +0 -7
  125. package/dist/proxy/handler/openai.js +0 -132
  126. package/dist/proxy/handler/proxy-handler.d.ts +0 -15
  127. package/dist/proxy/handler/proxy-handler.js +0 -430
  128. package/dist/proxy/handler/responses.d.ts +0 -7
  129. package/dist/proxy/handler/responses.js +0 -48
  130. package/dist/proxy/transform/transform-coordinator.d.ts +0 -12
  131. package/dist/proxy/transform/transform-coordinator.js +0 -151
  132. package/frontend-dist/assets/Login-CC1aMDOU.js +0 -1
  133. package/frontend-dist/assets/ModelMappings-BD7Xd9u_.js +0 -1
  134. package/frontend-dist/assets/QuickSetup-Cp1nlz0G.js +0 -1
  135. package/frontend-dist/assets/Schedules-C3JE7gox.js +0 -1
  136. package/frontend-dist/assets/useLogRetention-DZGdtwFB.js +0 -1
@@ -418,7 +418,7 @@ export const adminProviderRoutes = (app, options, done) => {
418
418
  return reply.send(modelIds);
419
419
  }
420
420
  catch (err) {
421
- const message = err instanceof Error ? err.message : String(err);
421
+ const message = err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err);
422
422
  return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, `连接上游失败: ${message}`));
423
423
  }
424
424
  });
@@ -18,6 +18,7 @@ import { adminQuickSetupRoutes } from "./quick-setup.js";
18
18
  import { adminImportExportRoutes } from "./settings-import-export.js";
19
19
  import { adminTransformRuleRoutes } from "./transform-rules.js";
20
20
  import { adminScheduleRoutes } from "./schedules.js";
21
+ import { hookRegistry } from "../proxy/pipeline/hook-registry.js";
21
22
  export const adminRoutes = (app, options, done) => {
22
23
  // Setup 路由不需要 auth
23
24
  app.register(adminSetupRoutes, { db: options.db });
@@ -41,5 +42,9 @@ export const adminRoutes = (app, options, done) => {
41
42
  app.register(adminQuickSetupRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController });
42
43
  app.register(adminUpgradeRoutes, { db: options.db, closeFn: options.closeFn ?? (async () => { }) });
43
44
  app.register(adminTransformRuleRoutes, { db: options.db, pluginRegistry: options.pluginRegistry });
45
+ // Pipeline hooks 查询
46
+ app.get("/admin/api/pipeline/hooks", async () => {
47
+ return { hooks: hookRegistry.getAll() };
48
+ });
44
49
  done();
45
50
  };
@@ -95,7 +95,7 @@ function checkOverlap(db, groupId, excludeId, weekDays, startHour, endHour) {
95
95
  }
96
96
  const HOUR_PAD_WIDTH = 2;
97
97
  function formatHour(h) {
98
- return String(h).padStart(HOUR_PAD_WIDTH, "0") + ":00";
98
+ return h.toString().padStart(HOUR_PAD_WIDTH, "0") + ":00";
99
99
  }
100
100
  export const adminScheduleRoutes = (app, options, done) => {
101
101
  const { db } = options;
@@ -93,7 +93,7 @@ export const adminUpgradeRoutes = (app, options, done) => {
93
93
  return reply.send({ ok: true, version });
94
94
  }
95
95
  catch (err) {
96
- const msg = err instanceof Error ? err.message : String(err);
96
+ const msg = err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err);
97
97
  return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `升级失败: ${msg}`));
98
98
  }
99
99
  });
@@ -125,7 +125,7 @@ export const adminUpgradeRoutes = (app, options, done) => {
125
125
  process.exit(0);
126
126
  }
127
127
  catch (err) {
128
- const msg = err instanceof Error ? err.message : String(err);
128
+ const msg = err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err);
129
129
  req.log.error({ err }, `Restart failed: ${msg}`);
130
130
  process.exit(1);
131
131
  }
@@ -162,7 +162,7 @@ export const adminUpgradeRoutes = (app, options, done) => {
162
162
  return reply.send({ ok: true });
163
163
  }
164
164
  catch (err) {
165
- const msg = err instanceof Error ? err.message : String(err);
165
+ const msg = err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err);
166
166
  return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `同步失败: ${msg}`));
167
167
  }
168
168
  });
@@ -12,7 +12,7 @@ function loadJson(filename) {
12
12
  return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
13
13
  }
14
14
  catch (err) {
15
- process.stderr.write(`[recommended] 加载 ${filename} 失败: ${err instanceof Error ? err.message : String(err)}\n`);
15
+ process.stderr.write(`[recommended] 加载 ${filename} 失败: ${err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err)}\n`);
16
16
  return [];
17
17
  }
18
18
  }
@@ -31,7 +31,7 @@ export function getConfigVersions() {
31
31
  return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
32
32
  }
33
33
  catch (err) {
34
- process.stderr.write(`[recommended] 加载 version.json 失败: ${err instanceof Error ? err.message : String(err)}\n`);
34
+ process.stderr.write(`[recommended] 加载 version.json 失败: ${err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err)}\n`);
35
35
  return { providers: 0, retryRules: 0 };
36
36
  }
37
37
  }
@@ -7,6 +7,7 @@ export declare const HTTP_CONFLICT = 409;
7
7
  export declare const HTTP_UNPROCESSABLE_ENTITY = 422;
8
8
  export declare const HTTP_INTERNAL_ERROR = 500;
9
9
  export declare const HTTP_BAD_GATEWAY = 502;
10
+ export declare const HTTP_CLIENT_CLOSED = 499;
10
11
  export declare const HTTP_SERVICE_UNAVAILABLE = 503;
11
12
  export declare const PROXY_API_TYPES: Record<string, string>;
12
13
  export declare function getProxyApiType(url: string): string | null;
@@ -9,6 +9,7 @@ export const HTTP_CONFLICT = 409;
9
9
  export const HTTP_UNPROCESSABLE_ENTITY = 422;
10
10
  export const HTTP_INTERNAL_ERROR = 500;
11
11
  export const HTTP_BAD_GATEWAY = 502;
12
+ export const HTTP_CLIENT_CLOSED = 499; // nginx convention: client disconnected
12
13
  export const HTTP_SERVICE_UNAVAILABLE = 503;
13
14
  // api_type 路由映射:proxy path → api type,用于全局 hook/errorHandler 中识别代理请求
14
15
  export const PROXY_API_TYPES = {
@@ -8,6 +8,7 @@ export declare const SERVICE_KEYS: {
8
8
  readonly sessionTracker: "sessionTracker";
9
9
  readonly adaptiveController: "adaptiveController";
10
10
  readonly pluginRegistry: "pluginRegistry";
11
+ readonly formatRegistry: "formatRegistry";
11
12
  readonly logFileWriter: "logFileWriter";
12
13
  readonly proxyAgentFactory: "proxyAgentFactory";
13
14
  };
@@ -8,6 +8,7 @@ export const SERVICE_KEYS = {
8
8
  sessionTracker: "sessionTracker",
9
9
  adaptiveController: "adaptiveController",
10
10
  pluginRegistry: "pluginRegistry",
11
+ formatRegistry: "formatRegistry",
11
12
  logFileWriter: "logFileWriter",
12
13
  proxyAgentFactory: "proxyAgentFactory",
13
14
  };
@@ -57,7 +57,7 @@ export function scheduleDbSizeMonitor(db, dbPath, options) {
57
57
  }
58
58
  catch (e) {
59
59
  // DB 可能已关闭(测试清理、进程关闭等)
60
- options.log.info(`Size monitor check skipped: ${e instanceof Error ? e.message : String(e)}`);
60
+ options.log.info(`Size monitor check skipped: ${e instanceof Error ? e.message : e instanceof Error ? e.message : JSON.stringify(e)}`);
61
61
  }
62
62
  finally {
63
63
  running = false;
@@ -29,7 +29,7 @@ export function scheduleLogCleanup(db, log) {
29
29
  }
30
30
  catch (e) {
31
31
  // DB 可能已关闭(测试清理、进程关闭等)
32
- log.info(`Log cleanup skipped: ${e instanceof Error ? e.message : String(e)}`);
32
+ log.info(`Log cleanup skipped: ${e instanceof Error ? e.message : e instanceof Error ? e.message : JSON.stringify(e)}`);
33
33
  }
34
34
  finally {
35
35
  cleaning = false;
@@ -14,7 +14,7 @@ export function getLogRetentionDays(db) {
14
14
  return val ? parseInt(val, 10) : DEFAULT_LOG_RETENTION_DAYS;
15
15
  }
16
16
  export function setLogRetentionDays(db, days) {
17
- setSetting(db, "log_retention_days", String(days));
17
+ setSetting(db, "log_retention_days", days.toString());
18
18
  }
19
19
  const DEFAULT_DB_MAX_SIZE_MB = 1024;
20
20
  const DEFAULT_LOG_TABLE_MAX_SIZE_MB = 800;
@@ -23,14 +23,14 @@ export function getDbMaxSizeMb(db) {
23
23
  return val ? parseInt(val, 10) : DEFAULT_DB_MAX_SIZE_MB;
24
24
  }
25
25
  export function setDbMaxSizeMb(db, mb) {
26
- setSetting(db, "db_max_size_mb", String(mb));
26
+ setSetting(db, "db_max_size_mb", mb.toString());
27
27
  }
28
28
  export function getLogTableMaxSizeMb(db) {
29
29
  const val = getSetting(db, "log_table_max_size_mb");
30
30
  return val ? parseInt(val, 10) : DEFAULT_LOG_TABLE_MAX_SIZE_MB;
31
31
  }
32
32
  export function setLogTableMaxSizeMb(db, mb) {
33
- setSetting(db, "log_table_max_size_mb", String(mb));
33
+ setSetting(db, "log_table_max_size_mb", mb.toString());
34
34
  }
35
35
  export function getConfigSyncSource(db) {
36
36
  const val = getSetting(db, "config_sync_source");
package/dist/index.js CHANGED
@@ -15,12 +15,20 @@ import { getConfig, getBaseConfig } from "./config/index.js";
15
15
  import { initDatabase, getAllProviders } from "./db/index.js";
16
16
  import { loadRecommendedConfig } from "./config/recommended.js";
17
17
  import { authMiddleware } from "./middleware/auth.js";
18
- import { openaiProxy } from "./proxy/handler/openai.js";
19
- import { anthropicProxy } from "./proxy/handler/anthropic.js";
20
- import { responsesProxy } from "./proxy/handler/responses.js";
18
+ import { createProxyHandler } from "./proxy/handler/create-proxy-handler.js";
21
19
  import { adminRoutes } from "./admin/routes.js";
22
20
  import { RetryRuleMatcher } from "./proxy/orchestration/retry-rules.js";
23
21
  import { PluginRegistry } from "./proxy/transform/plugin-registry.js";
22
+ import { FormatRegistry } from "./proxy/format/registry.js";
23
+ import { openaiAdapter } from "./proxy/format/adapters/openai.js";
24
+ import { anthropicAdapter } from "./proxy/format/adapters/anthropic.js";
25
+ import { responsesAdapter } from "./proxy/format/adapters/responses.js";
26
+ import { openaiToAnthropicConverter } from "./proxy/format/converters/openai-anthropic.js";
27
+ import { anthropicToOpenAIConverter } from "./proxy/format/converters/anthropic-openai.js";
28
+ import { openaiToResponsesConverter } from "./proxy/format/converters/openai-responses.js";
29
+ import { responsesToOpenAIConverter } from "./proxy/format/converters/responses-openai.js";
30
+ import { responsesToAnthropicConverter } from "./proxy/format/converters/responses-anthropic.js";
31
+ import { anthropicToResponsesConverter } from "./proxy/format/converters/anthropic-responses.js";
24
32
  import { SemaphoreManager, AdaptiveController } from "@llm-router/core/concurrency";
25
33
  import { RequestTracker } from "@llm-router/core/monitor";
26
34
  import { UsageWindowTracker } from "./proxy/routing/usage-window-tracker.js";
@@ -32,6 +40,7 @@ import fastifyStatic from "@fastify/static";
32
40
  import { ServiceContainer, SERVICE_KEYS } from "./core/container.js";
33
41
  import { LogFileWriter } from "./storage/log-file-writer.js";
34
42
  import { ProxyAgentFactory } from "./proxy/transport/proxy-agent.js";
43
+ import { registerBuiltinHooks } from "./proxy/pipeline/register-hooks.js";
35
44
  import { scheduleLogFileMaintenance } from "./storage/log-file-compressor.js";
36
45
  import { getDetailLogEnabled, getLogFileRetentionDays } from "./db/settings.js";
37
46
  import { dirname, join } from "node:path";
@@ -216,6 +225,18 @@ export async function buildApp(options) {
216
225
  const pluginsDir = path.resolve(__dirname, "../plugins/transform");
217
226
  pluginRegistry.scanPluginsDir(pluginsDir);
218
227
  container.register(SERVICE_KEYS.pluginRegistry, () => pluginRegistry);
228
+ // 注册 FormatRegistry(3 adapters + 6 converters 覆盖所有格式转换)
229
+ const formatRegistry = new FormatRegistry();
230
+ formatRegistry.registerAdapter(openaiAdapter);
231
+ formatRegistry.registerAdapter(anthropicAdapter);
232
+ formatRegistry.registerAdapter(responsesAdapter);
233
+ formatRegistry.registerConverter(openaiToAnthropicConverter);
234
+ formatRegistry.registerConverter(anthropicToOpenAIConverter);
235
+ formatRegistry.registerConverter(openaiToResponsesConverter);
236
+ formatRegistry.registerConverter(responsesToOpenAIConverter);
237
+ formatRegistry.registerConverter(responsesToAnthropicConverter);
238
+ formatRegistry.registerConverter(anthropicToResponsesConverter);
239
+ container.register(SERVICE_KEYS.formatRegistry, () => formatRegistry);
219
240
  // 注册 ProxyAgentFactory
220
241
  container.register(SERVICE_KEYS.proxyAgentFactory, () => new ProxyAgentFactory());
221
242
  // 从容器解析所有服务
@@ -230,9 +251,24 @@ export async function buildApp(options) {
230
251
  // 从 DB 读取已有 provider 的并发配置,初始化信号量/adaptive/tracker(共享逻辑)
231
252
  initializeProviderState(db, semaphoreManager, adaptiveController, tracker);
232
253
  app.register(authMiddleware, { db });
233
- app.register(openaiProxy, { db, container });
234
- app.register(anthropicProxy, { db, container });
235
- app.register(responsesProxy, { db, container });
254
+ // 注册内置 hooks hookRegistry(供 Admin API 查询)
255
+ registerBuiltinHooks();
256
+ // --- New pipeline-based proxy handlers (Phase 3) ---
257
+ const openaiHandler = createProxyHandler({
258
+ apiType: "openai",
259
+ paths: ["/v1/chat/completions", "/chat/completions"],
260
+ });
261
+ const anthropicHandler = createProxyHandler({
262
+ apiType: "anthropic",
263
+ paths: ["/v1/messages"],
264
+ });
265
+ const responsesHandler = createProxyHandler({
266
+ apiType: "openai-responses",
267
+ paths: ["/v1/responses", "/responses"],
268
+ });
269
+ app.register(openaiHandler, { db, container });
270
+ app.register(anthropicHandler, { db, container });
271
+ app.register(responsesHandler, { db, container });
236
272
  // StateRegistry — Admin 层通过此接口触发 proxy 层状态刷新,消除 admin→proxy 依赖
237
273
  const stateRegistry = {
238
274
  refreshRetryRules: () => matcher.load(db),
@@ -262,7 +298,7 @@ export async function buildApp(options) {
262
298
  });
263
299
  // SPA fallback: /admin/ 下非 API 路径返回 index.html
264
300
  app.setNotFoundHandler((request, reply) => {
265
- if (request.url.startsWith("/admin") &&
301
+ if ((request.url.startsWith("/admin/") || request.url === "/admin") &&
266
302
  !request.url.startsWith("/admin/api")) {
267
303
  return reply.sendFile("index.html");
268
304
  }
@@ -347,7 +383,7 @@ export async function main() {
347
383
  });
348
384
  process.on("unhandledRejection", (reason) => {
349
385
  try {
350
- app.log.error({ err: reason instanceof Error ? reason : new Error(String(reason)) }, "Unhandled rejection");
386
+ app.log.error({ err: reason instanceof Error ? reason : new Error(typeof reason === 'string' ? reason : JSON.stringify(reason)) }, "Unhandled rejection");
351
387
  /* eslint-disable taste/no-silent-catch -- app.log 可能已崩溃,console 是最后手段 */
352
388
  }
353
389
  catch {
@@ -0,0 +1,2 @@
1
+ import type { FormatAdapter } from "../types.js";
2
+ export declare const anthropicAdapter: FormatAdapter;
@@ -0,0 +1,18 @@
1
+ const ANTHROPIC_ERROR_META = {
2
+ modelNotFound: { type: "not_found_error", code: "model_not_found" },
3
+ modelNotAllowed: { type: "forbidden_error", code: "model_not_allowed" },
4
+ providerUnavailable: { type: "api_error", code: "provider_unavailable" },
5
+ providerTypeMismatch: { type: "api_error", code: "provider_type_mismatch" },
6
+ upstreamConnectionFailed: { type: "upstream_error", code: "upstream_connection_failed" },
7
+ concurrencyQueueFull: { type: "api_error", code: "concurrency_queue_full" },
8
+ concurrencyTimeout: { type: "api_error", code: "concurrency_timeout" },
9
+ promptTooLong: { type: "invalid_request_error", code: "context_window_exceeded" },
10
+ };
11
+ export const anthropicAdapter = {
12
+ apiType: "anthropic",
13
+ defaultPath: "/v1/messages",
14
+ errorMeta: ANTHROPIC_ERROR_META,
15
+ formatError(message) {
16
+ return { type: "error", error: { type: "api_error", message } };
17
+ },
18
+ };
@@ -0,0 +1,2 @@
1
+ import type { FormatAdapter } from "../types.js";
2
+ export declare const openaiAdapter: FormatAdapter;
@@ -0,0 +1,14 @@
1
+ import { OPENAI_FAMILY_ERROR_META } from "./shared-error-meta.js";
2
+ export const openaiAdapter = {
3
+ apiType: "openai",
4
+ defaultPath: "/v1/chat/completions",
5
+ errorMeta: OPENAI_FAMILY_ERROR_META,
6
+ beforeSendProxy(body, isStream) {
7
+ if (isStream && !body.stream_options) {
8
+ body.stream_options = { include_usage: true };
9
+ }
10
+ },
11
+ formatError(message, code) {
12
+ return { error: { message, type: "upstream_error", code: code ?? "upstream_error" } };
13
+ },
14
+ };
@@ -0,0 +1,2 @@
1
+ import type { FormatAdapter } from "../types.js";
2
+ export declare const responsesAdapter: FormatAdapter;
@@ -0,0 +1,9 @@
1
+ import { OPENAI_FAMILY_ERROR_META } from "./shared-error-meta.js";
2
+ export const responsesAdapter = {
3
+ apiType: "openai-responses",
4
+ defaultPath: "/v1/responses",
5
+ errorMeta: OPENAI_FAMILY_ERROR_META,
6
+ formatError(message, code) {
7
+ return { error: { message, type: "invalid_request_error", code: code ?? "upstream_error" } };
8
+ },
9
+ };
@@ -0,0 +1,9 @@
1
+ import type { ErrorKind } from "../types.js";
2
+ /**
3
+ * OpenAI 和 Responses API 共用的错误元数据。
4
+ * 两者 error code/type 完全相同,仅 apiType/defaultPath/formatError 不同。
5
+ */
6
+ export declare const OPENAI_FAMILY_ERROR_META: Record<ErrorKind, {
7
+ type: string;
8
+ code: string;
9
+ }>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * OpenAI 和 Responses API 共用的错误元数据。
3
+ * 两者 error code/type 完全相同,仅 apiType/defaultPath/formatError 不同。
4
+ */
5
+ export const OPENAI_FAMILY_ERROR_META = {
6
+ modelNotFound: { type: "invalid_request_error", code: "model_not_found" },
7
+ modelNotAllowed: { type: "invalid_request_error", code: "model_not_allowed" },
8
+ providerUnavailable: { type: "server_error", code: "provider_unavailable" },
9
+ providerTypeMismatch: { type: "server_error", code: "provider_type_mismatch" },
10
+ upstreamConnectionFailed: { type: "upstream_error", code: "upstream_connection_failed" },
11
+ concurrencyQueueFull: { type: "server_error", code: "concurrency_queue_full" },
12
+ concurrencyTimeout: { type: "server_error", code: "concurrency_timeout" },
13
+ promptTooLong: { type: "invalid_request_error", code: "context_window_exceeded" },
14
+ };
@@ -0,0 +1 @@
1
+ export declare const anthropicToOpenAIConverter: import("../types.js").FormatConverter;
@@ -0,0 +1,11 @@
1
+ import { createConverter } from "../types.js";
2
+ import { anthropicToOpenAIRequest } from "../../transform/request-transform.js";
3
+ import { anthropicResponseToOpenAI } from "../../transform/response-transform.js";
4
+ import { AnthropicToOpenAITransform } from "../../transform/stream-ant2oa.js";
5
+ export const anthropicToOpenAIConverter = createConverter({
6
+ sourceType: "anthropic",
7
+ targetType: "openai",
8
+ requestTransform: anthropicToOpenAIRequest,
9
+ responseTransform: anthropicResponseToOpenAI,
10
+ streamTransformClass: AnthropicToOpenAITransform,
11
+ });
@@ -0,0 +1 @@
1
+ export declare const anthropicToResponsesConverter: import("../types.js").FormatConverter;
@@ -0,0 +1,11 @@
1
+ import { createConverter } from "../types.js";
2
+ import { anthropicToResponsesRequest } from "../../transform/request-transform-responses.js";
3
+ import { anthropicToResponsesResponse } from "../../transform/response-transform-responses.js";
4
+ import { AnthropicToResponsesTransform } from "../../transform/stream-ant2resp.js";
5
+ export const anthropicToResponsesConverter = createConverter({
6
+ sourceType: "anthropic",
7
+ targetType: "openai-responses",
8
+ requestTransform: anthropicToResponsesRequest,
9
+ responseTransform: anthropicToResponsesResponse,
10
+ streamTransformClass: AnthropicToResponsesTransform,
11
+ });
@@ -0,0 +1 @@
1
+ export declare const openaiToAnthropicConverter: import("../types.js").FormatConverter;
@@ -0,0 +1,11 @@
1
+ import { createConverter } from "../types.js";
2
+ import { openaiToAnthropicRequest } from "../../transform/request-transform.js";
3
+ import { openaiResponseToAnthropic } from "../../transform/response-transform.js";
4
+ import { OpenAIToAnthropicTransform } from "../../transform/stream-oa2ant.js";
5
+ export const openaiToAnthropicConverter = createConverter({
6
+ sourceType: "openai",
7
+ targetType: "anthropic",
8
+ requestTransform: openaiToAnthropicRequest,
9
+ responseTransform: openaiResponseToAnthropic,
10
+ streamTransformClass: OpenAIToAnthropicTransform,
11
+ });
@@ -0,0 +1 @@
1
+ export declare const openaiToResponsesConverter: import("../types.js").FormatConverter;
@@ -0,0 +1,11 @@
1
+ import { createConverter } from "../types.js";
2
+ import { chatToResponsesRequest } from "../../transform/request-bridge-responses.js";
3
+ import { chatToResponsesResponse } from "../../transform/response-bridge-responses.js";
4
+ import { ChatToResponsesBridgeTransform } from "../../transform/stream-bridge-chat2resp.js";
5
+ export const openaiToResponsesConverter = createConverter({
6
+ sourceType: "openai",
7
+ targetType: "openai-responses",
8
+ requestTransform: chatToResponsesRequest,
9
+ responseTransform: chatToResponsesResponse,
10
+ streamTransformClass: ChatToResponsesBridgeTransform,
11
+ });
@@ -0,0 +1 @@
1
+ export declare const responsesToAnthropicConverter: import("../types.js").FormatConverter;
@@ -0,0 +1,11 @@
1
+ import { createConverter } from "../types.js";
2
+ import { responsesToAnthropicRequest } from "../../transform/request-transform-responses.js";
3
+ import { responsesToAnthropicResponse } from "../../transform/response-transform-responses.js";
4
+ import { ResponsesToAnthropicTransform } from "../../transform/stream-resp2ant.js";
5
+ export const responsesToAnthropicConverter = createConverter({
6
+ sourceType: "openai-responses",
7
+ targetType: "anthropic",
8
+ requestTransform: responsesToAnthropicRequest,
9
+ responseTransform: responsesToAnthropicResponse,
10
+ streamTransformClass: ResponsesToAnthropicTransform,
11
+ });
@@ -0,0 +1 @@
1
+ export declare const responsesToOpenAIConverter: import("../types.js").FormatConverter;
@@ -0,0 +1,11 @@
1
+ import { createConverter } from "../types.js";
2
+ import { responsesToChatRequest } from "../../transform/request-bridge-responses.js";
3
+ import { responsesToChatResponse } from "../../transform/response-bridge-responses.js";
4
+ import { ResponsesToChatBridgeTransform } from "../../transform/stream-bridge-resp2chat.js";
5
+ export const responsesToOpenAIConverter = createConverter({
6
+ sourceType: "openai-responses",
7
+ targetType: "openai",
8
+ requestTransform: responsesToChatRequest,
9
+ responseTransform: responsesToChatResponse,
10
+ streamTransformClass: ResponsesToChatBridgeTransform,
11
+ });
@@ -0,0 +1,17 @@
1
+ import type { Transform } from "stream";
2
+ import type { FormatAdapter, FormatConverter } from "./types.js";
3
+ export declare class FormatRegistry {
4
+ private adapters;
5
+ private converters;
6
+ registerAdapter(adapter: FormatAdapter): void;
7
+ registerConverter(converter: FormatConverter): void;
8
+ getAdapter(apiType: string): FormatAdapter | undefined;
9
+ needsTransform(source: string, target: string): boolean;
10
+ transformRequest(body: Record<string, unknown>, source: string, target: string, model: string): {
11
+ body: Record<string, unknown>;
12
+ upstreamPath: string;
13
+ };
14
+ transformResponse(bodyStr: string, source: string, target: string): string;
15
+ transformError(bodyStr: string, source: string, target: string): string;
16
+ createStreamTransform(source: string, target: string, model: string): Transform | undefined;
17
+ }
@@ -0,0 +1,50 @@
1
+ export class FormatRegistry {
2
+ adapters = new Map();
3
+ converters = new Map();
4
+ registerAdapter(adapter) {
5
+ this.adapters.set(adapter.apiType, adapter);
6
+ }
7
+ registerConverter(converter) {
8
+ this.converters.set(`${converter.sourceType}→${converter.targetType}`, converter);
9
+ }
10
+ getAdapter(apiType) {
11
+ return this.adapters.get(apiType);
12
+ }
13
+ needsTransform(source, target) {
14
+ return source !== target;
15
+ }
16
+ transformRequest(body, source, target, model) {
17
+ const targetAdapter = this.adapters.get(target);
18
+ const upstreamPath = targetAdapter?.defaultPath ?? "/v1/chat/completions";
19
+ const converter = this.converters.get(`${source}→${target}`);
20
+ if (!converter)
21
+ return { body, upstreamPath };
22
+ return { body: converter.transformRequest(body, model), upstreamPath };
23
+ }
24
+ transformResponse(bodyStr, source, target) {
25
+ const converter = this.converters.get(`${source}→${target}`);
26
+ if (!converter)
27
+ return bodyStr;
28
+ return converter.transformResponse(bodyStr);
29
+ }
30
+ transformError(bodyStr, source, target) {
31
+ if (source === target)
32
+ return bodyStr;
33
+ try {
34
+ const parsed = JSON.parse(bodyStr);
35
+ const message = parsed.error?.message ?? parsed.message ?? JSON.stringify(parsed);
36
+ const code = parsed.error?.code ?? parsed.code;
37
+ const targetAdapter = this.adapters.get(target);
38
+ if (!targetAdapter)
39
+ return bodyStr;
40
+ return JSON.stringify(targetAdapter.formatError(message, code));
41
+ }
42
+ catch {
43
+ return bodyStr;
44
+ }
45
+ }
46
+ createStreamTransform(source, target, model) {
47
+ const converter = this.converters.get(`${source}→${target}`);
48
+ return converter?.createStreamTransform(model);
49
+ }
50
+ }
@@ -0,0 +1,27 @@
1
+ import type { Transform } from "stream";
2
+ export type ErrorKind = "modelNotFound" | "modelNotAllowed" | "providerUnavailable" | "providerTypeMismatch" | "upstreamConnectionFailed" | "concurrencyQueueFull" | "concurrencyTimeout" | "promptTooLong";
3
+ export interface FormatAdapter {
4
+ readonly apiType: string;
5
+ readonly defaultPath: string;
6
+ readonly errorMeta: Record<ErrorKind, {
7
+ type: string;
8
+ code: string;
9
+ }>;
10
+ beforeSendProxy?(body: Record<string, unknown>, isStream: boolean): void;
11
+ formatError(message: string, code?: string): unknown;
12
+ }
13
+ export interface FormatConverter {
14
+ readonly sourceType: string;
15
+ readonly targetType: string;
16
+ transformRequest(body: Record<string, unknown>, model: string): Record<string, unknown>;
17
+ transformResponse(bodyStr: string): string;
18
+ createStreamTransform(model: string): Transform;
19
+ }
20
+ /** Factory: eliminates repetitive object literal structure across 6 converters. */
21
+ export declare function createConverter(deps: {
22
+ sourceType: string;
23
+ targetType: string;
24
+ requestTransform: (body: Record<string, unknown>) => Record<string, unknown>;
25
+ responseTransform: (bodyStr: string) => string;
26
+ streamTransformClass: new (model: string) => Transform;
27
+ }): FormatConverter;
@@ -0,0 +1,16 @@
1
+ /** Factory: eliminates repetitive object literal structure across 6 converters. */
2
+ export function createConverter(deps) {
3
+ return {
4
+ sourceType: deps.sourceType,
5
+ targetType: deps.targetType,
6
+ transformRequest(body) {
7
+ return deps.requestTransform(body);
8
+ },
9
+ transformResponse(bodyStr) {
10
+ return deps.responseTransform(bodyStr);
11
+ },
12
+ createStreamTransform(model) {
13
+ return new deps.streamTransformClass(model);
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,14 @@
1
+ import type { FastifyPluginCallback } from "fastify";
2
+ import Database from "better-sqlite3";
3
+ import type { ServiceContainer } from "../../core/container.js";
4
+ export interface ProxyHandlerConfig {
5
+ /** API 类型:openai | openai-responses | anthropic */
6
+ apiType: "openai" | "openai-responses" | "anthropic";
7
+ /** 注册 POST 路由的路径列表 */
8
+ paths: string[];
9
+ }
10
+ export interface ProxyHandlerOptions {
11
+ db: Database.Database;
12
+ container: ServiceContainer;
13
+ }
14
+ export declare function createProxyHandler(config: ProxyHandlerConfig): FastifyPluginCallback<ProxyHandlerOptions>;