llm-simple-router 1.0.20 → 1.0.22

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 (90) hide show
  1. package/dist/admin/groups.js +1 -1
  2. package/dist/admin/logs.js +1 -1
  3. package/dist/admin/mappings.js +1 -1
  4. package/dist/admin/monitor.js +1 -1
  5. package/dist/admin/providers.d.ts +4 -2
  6. package/dist/admin/providers.js +7 -16
  7. package/dist/admin/proxy-enhancement.d.ts +2 -0
  8. package/dist/admin/proxy-enhancement.js +2 -3
  9. package/dist/admin/quick-setup.js +2 -2
  10. package/dist/admin/retry-rules.js +1 -1
  11. package/dist/admin/router-keys.js +1 -1
  12. package/dist/admin/routes.d.ts +4 -2
  13. package/dist/admin/routes.js +3 -4
  14. package/dist/admin/schedules.js +3 -40
  15. package/dist/admin/settings.js +1 -1
  16. package/dist/admin/setup.js +1 -1
  17. package/dist/admin/utils.d.ts +11 -0
  18. package/dist/admin/utils.js +57 -0
  19. package/dist/core/log-detail-policy.d.ts +12 -0
  20. package/dist/core/log-detail-policy.js +21 -0
  21. package/dist/core/provider-connectivity.d.ts +22 -0
  22. package/dist/core/provider-connectivity.js +5 -0
  23. package/dist/core/proxy-agent-types.d.ts +9 -0
  24. package/dist/core/proxy-agent-types.js +6 -0
  25. package/dist/core/registry.d.ts +7 -0
  26. package/dist/db/logs.d.ts +1 -1
  27. package/dist/db/logs.js +1 -1
  28. package/dist/index.js +7 -1
  29. package/dist/proxy/log-detail-policy.d.ts +1 -12
  30. package/dist/proxy/log-detail-policy.js +2 -20
  31. package/dist/proxy/transport/provider-connectivity.d.ts +11 -0
  32. package/dist/proxy/transport/provider-connectivity.js +11 -0
  33. package/dist/proxy/transport/proxy-agent.d.ts +2 -1
  34. package/frontend-dist/assets/{AuthLayout-BwI_T7qz.js → AuthLayout-BvYyVNay.js} +1 -1
  35. package/frontend-dist/assets/{Card-C5jCkE1m.js → Card-CrAceTFb.js} +1 -1
  36. package/frontend-dist/assets/{CardContent-Dozca7Pu.js → CardContent-otK_vTWx.js} +1 -1
  37. package/frontend-dist/assets/{CardTitle-DqrzLNUU.js → CardTitle-BEMKGuDs.js} +1 -1
  38. package/frontend-dist/assets/{CascadingModelSelect-D2DRpM28.js → CascadingModelSelect-TP3nDN4K.js} +1 -1
  39. package/frontend-dist/assets/{Checkbox-B-BvqDQI.js → Checkbox-KBtwJU8Q.js} +1 -1
  40. package/frontend-dist/assets/{CollapsibleContent-BV1VGpMl.js → CollapsibleContent-CK4MEdEk.js} +1 -1
  41. package/frontend-dist/assets/{CollapsibleTrigger-BNuoyr3g.js → CollapsibleTrigger-C1YuylWt.js} +1 -1
  42. package/frontend-dist/assets/{ConcurrencyControl-XSeHDo1-.js → ConcurrencyControl-naUxnJl6.js} +1 -1
  43. package/frontend-dist/assets/{Dashboard-CsgO9KeQ.js → Dashboard-CsMzPzyB.js} +1 -1
  44. package/frontend-dist/assets/{Input-C5OLhaEA.js → Input-CISfShtH.js} +1 -1
  45. package/frontend-dist/assets/{Label-B5FJLcPy.js → Label-CpOxo1sn.js} +1 -1
  46. package/frontend-dist/assets/{Login-wZ8_XeqQ.js → Login-DWIJrfGr.js} +1 -1
  47. package/frontend-dist/assets/{Logs-CiC8wTHb.js → Logs-Do6F6xpO.js} +1 -1
  48. package/frontend-dist/assets/{ModelMappings-CZcMgWbM.js → ModelMappings-BeCtJid-.js} +1 -1
  49. package/frontend-dist/assets/{Monitor-Cb4RhaXB.js → Monitor-BkKdwtEO.js} +1 -1
  50. package/frontend-dist/assets/{Providers-DGwBmDka.js → Providers-DZDMM3Ly.js} +1 -1
  51. package/frontend-dist/assets/{ProxyEnhancement-Duyvg2Pu.js → ProxyEnhancement-Bf9reTLr.js} +1 -1
  52. package/frontend-dist/assets/{QuickSetup-MgM8jncN.js → QuickSetup-D05Pbvny.js} +1 -1
  53. package/frontend-dist/assets/{RetryRules-CucoGQb3.js → RetryRules-ZYSMISJv.js} +1 -1
  54. package/frontend-dist/assets/{RouterKeys-Cj46NlnE.js → RouterKeys-DyCXVjSV.js} +1 -1
  55. package/frontend-dist/assets/{RovingFocusItem-zXj-QlAl.js → RovingFocusItem-DyThG4Ao.js} +1 -1
  56. package/frontend-dist/assets/{Schedules-BxtgE_jQ.js → Schedules-BMdkLY7a.js} +1 -1
  57. package/frontend-dist/assets/{Separator-Cmw60D6s.js → Separator-Cge3lk25.js} +1 -1
  58. package/frontend-dist/assets/{Settings-CSjzJJ1L.js → Settings-CqA17rFr.js} +1 -1
  59. package/frontend-dist/assets/{Setup-P3Amua1-.js → Setup-C03USty0.js} +1 -1
  60. package/frontend-dist/assets/{Skeleton-Bb6JdseU.js → Skeleton-DBYFTs7B.js} +1 -1
  61. package/frontend-dist/assets/{Switch-G855OsUp.js → Switch-CLdGo10G.js} +1 -1
  62. package/frontend-dist/assets/{TableHeader-CFMCLKRy.js → TableHeader-BmXchBD9.js} +1 -1
  63. package/frontend-dist/assets/{TabsTrigger-P5C-goq6.js → TabsTrigger-URUZbzCb.js} +1 -1
  64. package/frontend-dist/assets/{UnifiedRequestDialog-b0YkEvxq.js → UnifiedRequestDialog-ZW5x63E9.js} +1 -1
  65. package/frontend-dist/assets/{VisuallyHiddenInput-De_zFaJT.js → VisuallyHiddenInput-Bi3I1T8I.js} +1 -1
  66. package/frontend-dist/assets/arrow-down-BCM-l0Jl.js +1 -0
  67. package/frontend-dist/assets/{badge-BKjuTJ01.js → badge-eqb4qgGp.js} +1 -1
  68. package/frontend-dist/assets/{button-DpPvjx0f.js → button-mHNAa2aF.js} +2 -2
  69. package/frontend-dist/assets/chevron-right-D0wqOqtp.js +1 -0
  70. package/frontend-dist/assets/{dialog-BxDRyeVd.js → dialog-BBMCB4Zc.js} +1 -1
  71. package/frontend-dist/assets/{image-Bp59tVen.js → image-DTqw64FB.js} +1 -1
  72. package/frontend-dist/assets/{index-Di7f6Crg.js → index-Ddg5hheZ.js} +2 -2
  73. package/frontend-dist/assets/{model-patches-CGEItLFI.js → model-patches-Bru1xRNm.js} +1 -1
  74. package/frontend-dist/assets/{pencil-d2ST8J0H.js → pencil-B6uEYOlv.js} +1 -1
  75. package/frontend-dist/assets/plus-B2f9w-px.js +1 -0
  76. package/frontend-dist/assets/search-CczkyuG-.js +1 -0
  77. package/frontend-dist/assets/{sparkles-C1RBYUoM.js → sparkles-DyakAPX_.js} +1 -1
  78. package/frontend-dist/assets/{transform-domain-CaOlRFL4.js → transform-domain-BYHRTHa-.js} +1 -1
  79. package/frontend-dist/assets/{trash-2-C1YCkSOe.js → trash-2-KJKHmeJP.js} +1 -1
  80. package/frontend-dist/assets/{useClipboard-DfhNcDMZ.js → useClipboard-2juraaSb.js} +1 -1
  81. package/frontend-dist/assets/{useLogRetention-Ck3INr4l.js → useLogRetention-CYfHJhhs.js} +1 -1
  82. package/frontend-dist/assets/{useProviderGroups-IgmEpHao.js → useProviderGroups-Cg-m09or.js} +1 -1
  83. package/frontend-dist/index.html +2 -2
  84. package/package.json +1 -1
  85. package/dist/admin/constants.d.ts +0 -1
  86. package/dist/admin/constants.js +0 -2
  87. package/frontend-dist/assets/arrow-down-C0j8UmLw.js +0 -1
  88. package/frontend-dist/assets/chevron-right-QdaibhaO.js +0 -1
  89. package/frontend-dist/assets/plus-DPWmbUOf.js +0 -1
  90. package/frontend-dist/assets/search-BNr8kuWX.js +0 -1
@@ -2,7 +2,7 @@ import { Type } from "@sinclair/typebox";
2
2
  import { getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getProviderById, getMappingGroupById, getAllProviders, } from "../db/index.js";
3
3
  import { getSetting } from "../db/settings.js";
4
4
  import { serializeProviders } from "./providers.js";
5
- import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_CONFLICT, HTTP_NOT_FOUND } from "./constants.js";
5
+ import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_CONFLICT, HTTP_NOT_FOUND } from "../core/constants.js";
6
6
  import { parseModels } from "../config/model-context.js";
7
7
  import { API_CODE, apiError } from "./api-response.js";
8
8
  const CreateGroupSchema = Type.Object({
@@ -1,7 +1,7 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getRequestLogs, getRequestLogsGrouped, getRequestLogById, getRequestLogChildren, deleteLogsBefore, extractThinkingLevel, getAllProviders, getAllRouterKeys, getAllMappingGroups } from "../db/index.js";
3
3
  import { getLogRetentionDays } from "../db/settings.js";
4
- import { HTTP_NOT_FOUND } from "./constants.js";
4
+ import { HTTP_NOT_FOUND } from "../core/constants.js";
5
5
  import { API_CODE, apiError } from "./api-response.js";
6
6
  const LogQuerySchema = Type.Object({
7
7
  page: Type.Optional(Type.String()),
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getProviderById, getMappingGroupById, getMappingGroup, } from "../db/index.js";
3
- import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT } from "./constants.js";
3
+ import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT } from "../core/constants.js";
4
4
  import { API_CODE, apiError } from "./api-response.js";
5
5
  const CreateMappingSchema = Type.Object({
6
6
  client_model: Type.String({ minLength: 1 }),
@@ -1,5 +1,5 @@
1
1
  import { adaptSSEClient } from "../core/sse-client-adapter.js";
2
- import { HTTP_NOT_FOUND } from "./constants.js";
2
+ import { HTTP_NOT_FOUND } from "../core/constants.js";
3
3
  import { API_CODE, apiError } from "./api-response.js";
4
4
  const HTTP_OK = 200;
5
5
  export const adminMonitorRoutes = (app, options, done) => {
@@ -4,13 +4,15 @@ import type { Provider } from "../db/index.js";
4
4
  import type { StateRegistry } from "../core/registry.js";
5
5
  import type { AdaptiveController } from "../core/concurrency/index.js";
6
6
  import type { RequestTracker } from "../core/monitor/index.js";
7
- import type { ProxyAgentFactory } from "../proxy/transport/proxy-agent.js";
7
+ import type { ProviderConnectivityChecker } from "../core/provider-connectivity.js";
8
+ import type { IProxyAgentInvalidator } from "../core/proxy-agent-types.js";
8
9
  interface ProviderRoutesOptions {
9
10
  db: Database.Database;
10
11
  stateRegistry?: StateRegistry;
11
12
  tracker?: RequestTracker;
12
13
  adaptiveController?: AdaptiveController;
13
- proxyAgentFactory?: ProxyAgentFactory;
14
+ proxyAgentFactory?: IProxyAgentInvalidator;
15
+ connectivityChecker?: ProviderConnectivityChecker;
14
16
  }
15
17
  /** 序列化 provider 列表,解密敏感字段、展开 models/endpoints。供多个端点复用 */
16
18
  export declare function serializeProviders(db: Database.Database, providers: Provider[], encryptionKey: string, concurrencyStatus?: (id: string) => {
@@ -4,12 +4,10 @@ import { parseEndpoints, serializeEndpoints } from "../db/providers.js";
4
4
  import { getRecommendedProviders } from "../config/recommended.js";
5
5
  import { encrypt, decrypt } from "../utils/crypto.js";
6
6
  import { getSetting } from "../db/settings.js";
7
- import { HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_BAD_REQUEST, HTTP_OK } from "./constants.js";
7
+ import { HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_BAD_REQUEST, HTTP_OK, HTTP_SERVICE_UNAVAILABLE } from "../core/constants.js";
8
8
  import { API_CODE, apiError } from "./api-response.js";
9
9
  import { parseModels, buildModelInfoList, normalizePatchName, lookupCapabilities } from "../config/model-context.js";
10
10
  import { getModelInfoForProvider, setModelInfoForProvider, deleteAllModelInfoForProvider } from "../db/model-info.js";
11
- import { buildUpstreamHeaders } from "../proxy/proxy-core.js";
12
- import { callGet } from "../proxy/transport/http.js";
13
11
  const API_KEY_PREVIEW_MIN_LENGTH = 8;
14
12
  const FETCH_MODELS_BODY_PREVIEW_LENGTH = 200;
15
13
  function cascadeProviderDisable(db, providerId) {
@@ -87,17 +85,7 @@ function extractModelOverrides(models) {
87
85
  return { entries, overrides };
88
86
  }
89
87
  const API_KEY_PREVIEW_PREFIX_LEN = 4;
90
- const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
91
- /** 校验 base_url 是否为合法的 HTTP(S) URL */
92
- function isValidHttpUrl(str) {
93
- try {
94
- const url = new URL(str);
95
- return url.protocol === "http:" || url.protocol === "https:";
96
- }
97
- catch {
98
- return false;
99
- }
100
- }
88
+ import { PROVIDER_NAME_RE, isValidHttpUrl } from "./utils.js";
101
89
  /** 校验 endpoints 数组并加密 api_key。成功返回处理后的数组,失败返回错误信息 */
102
90
  function validateAndEncryptEndpoints(endpoints, encryptionKey) {
103
91
  const apiTypes = endpoints.map(e => e.api_type);
@@ -304,7 +292,7 @@ export function serializeProviders(db, providers, encryptionKey, concurrencyStat
304
292
  });
305
293
  }
306
294
  export const adminProviderRoutes = (app, options, done) => {
307
- const { db, stateRegistry, tracker, adaptiveController, proxyAgentFactory } = options;
295
+ const { db, stateRegistry, tracker, adaptiveController, proxyAgentFactory, connectivityChecker } = options;
308
296
  app.get("/admin/api/providers", async (_request, reply) => {
309
297
  const encryptionKey = getSetting(db, "encryption_key");
310
298
  const providers = getAllProviders(db);
@@ -523,8 +511,11 @@ export const adminProviderRoutes = (app, options, done) => {
523
511
  const { base_url, models_endpoint, api_key, api_type } = request.body;
524
512
  const backend = { base_url };
525
513
  const clientHeaders = {};
514
+ if (!connectivityChecker) {
515
+ return reply.code(HTTP_SERVICE_UNAVAILABLE).send(apiError(API_CODE.BAD_REQUEST, "Connectivity checker not available"));
516
+ }
526
517
  try {
527
- const result = await callGet(backend, api_key, clientHeaders, models_endpoint, (cliHdrs, key) => buildUpstreamHeaders(cliHdrs, key, undefined, api_type));
518
+ const result = await connectivityChecker.fetchModels(backend, api_key, clientHeaders, models_endpoint, api_type);
528
519
  if (result.statusCode !== HTTP_OK) {
529
520
  return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, `上游返回 HTTP ${result.statusCode}: ${result.body.substring(0, FETCH_MODELS_BODY_PREVIEW_LENGTH)}`));
530
521
  }
@@ -1,7 +1,9 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
+ import type { StateRegistry } from "../core/registry.js";
3
4
  interface ProxyEnhancementOptions {
4
5
  db: Database.Database;
6
+ stateRegistry?: StateRegistry;
5
7
  }
6
8
  export declare const adminProxyEnhancementRoutes: FastifyPluginCallback<ProxyEnhancementOptions>;
7
9
  export {};
@@ -1,6 +1,5 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getSetting, setSetting } from "../db/settings.js";
3
- import { clearEnhancementConfigCache } from "../proxy/routing/enhancement-config.js";
4
3
  import { getAllProviders } from "../db/index.js";
5
4
  import { serializeProviders } from "./providers.js";
6
5
  const UpdateProxyEnhancementSchema = Type.Object({
@@ -14,7 +13,7 @@ const UpdateProxyEnhancementSchema = Type.Object({
14
13
  ])),
15
14
  });
16
15
  export const adminProxyEnhancementRoutes = (app, options, done) => {
17
- const { db } = options;
16
+ const { db, stateRegistry } = options;
18
17
  app.get("/admin/api/proxy-enhancement", async (_request, reply) => {
19
18
  const raw = getSetting(db, "proxy_enhancement");
20
19
  const defaults = { tool_call_loop_enabled: false, stream_loop_enabled: false, tool_round_limit_enabled: true, tool_error_logging_enabled: false };
@@ -54,7 +53,7 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
54
53
  tool_error_logging_enabled: enhancementFields.tool_error_logging_enabled,
55
54
  };
56
55
  setSetting(db, "proxy_enhancement", JSON.stringify(config));
57
- clearEnhancementConfigCache();
56
+ stateRegistry?.clearEnhancementCache();
58
57
  // ai_retry_config is stored in a separate settings key
59
58
  if (ai_retry_config !== undefined) {
60
59
  setSetting(db, "ai_retry_config", ai_retry_config ? JSON.stringify(ai_retry_config) : "");
@@ -11,9 +11,9 @@ import { getRecommendedProviders, getRecommendedRetryRules } from "../config/rec
11
11
  import { lookupCapabilities } from "../config/model-context.js";
12
12
  import { getAllMappingGroups, getAllProviders } from "../db/index.js";
13
13
  import { serializeProviders } from "./providers.js";
14
- import { HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_BAD_GATEWAY, HTTP_CONFLICT } from "./constants.js";
14
+ import { HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_BAD_GATEWAY, HTTP_CONFLICT } from "../core/constants.js";
15
15
  import { API_CODE, apiError } from "./api-response.js";
16
- const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
16
+ import { PROVIDER_NAME_RE } from "./utils.js";
17
17
  const API_KEY_PREVIEW_MIN_LENGTH = 8;
18
18
  const API_KEY_PREVIEW_PREFIX_LEN = 4;
19
19
  const NEW_PROVIDER_ID = "__new__";
@@ -7,7 +7,7 @@ import { getRequestLogById } from "../db/logs.js";
7
7
  import { getProviderById } from "../db/providers.js";
8
8
  import { getSetting } from "../db/settings.js";
9
9
  import { decrypt } from "../utils/crypto.js";
10
- import { HTTP_OK, HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND } from "./constants.js";
10
+ import { HTTP_OK, HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND } from "../core/constants.js";
11
11
  import { API_CODE, apiError } from "./api-response.js";
12
12
  // AI 重试规则的 system prompt 模板(内联避免运行时文件依赖)
13
13
  const AI_RETRY_PROMPT_TEMPLATE = `You are an API retry rule expert. Your ONLY job is to analyze the error response and output a JSON retry rule.
@@ -3,7 +3,7 @@ import { Type } from "@sinclair/typebox";
3
3
  import { encrypt, decrypt } from "../utils/crypto.js";
4
4
  import { getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "../db/index.js";
5
5
  import { getSetting } from "../db/settings.js";
6
- import { HTTP_CREATED, HTTP_NOT_FOUND } from "./constants.js";
6
+ import { HTTP_CREATED, HTTP_NOT_FOUND } from "../core/constants.js";
7
7
  import { API_CODE, apiError } from "./api-response.js";
8
8
  const KEY_RANDOM_BYTES = 32;
9
9
  const KEY_PREFIX_LENGTH = 8;
@@ -3,7 +3,8 @@ import Database from "better-sqlite3";
3
3
  import type { StateRegistry } from "../core/registry.js";
4
4
  import type { RequestTracker } from "../core/monitor/index.js";
5
5
  import type { AdaptiveController } from "../core/concurrency/index.js";
6
- import type { ProxyAgentFactory } from "../proxy/transport/proxy-agent.js";
6
+ import type { IProxyAgentInvalidator } from "../core/proxy-agent-types.js";
7
+ import type { ProviderConnectivityChecker } from "../core/provider-connectivity.js";
7
8
  interface AdminRoutesOptions {
8
9
  db: Database.Database;
9
10
  stateRegistry: StateRegistry;
@@ -13,7 +14,8 @@ interface AdminRoutesOptions {
13
14
  logsDir?: string;
14
15
  pluginRegistry?: import("../proxy/transform/plugin-registry.js").PluginRegistry;
15
16
  closeFn?: () => Promise<void>;
16
- proxyAgentFactory?: ProxyAgentFactory;
17
+ proxyAgentFactory?: IProxyAgentInvalidator;
18
+ connectivityChecker?: ProviderConnectivityChecker;
17
19
  }
18
20
  export declare const adminRoutes: FastifyPluginCallback<AdminRoutesOptions>;
19
21
  export {};
@@ -19,13 +19,12 @@ import { adminImportExportRoutes } from "./settings-import-export.js";
19
19
  import { adminTransformRuleRoutes } from "./transform-rules.js";
20
20
  import { adminDashboardRoutes } from "./dashboard.js";
21
21
  import { adminScheduleRoutes } from "./schedules.js";
22
- import { hookRegistry } from "../proxy/pipeline/hook-registry.js";
23
22
  export const adminRoutes = (app, options, done) => {
24
23
  // Setup 路由不需要 auth
25
24
  app.register(adminSetupRoutes, { db: options.db });
26
25
  app.register(adminAuthPlugin, { db: options.db });
27
26
  app.register(adminLoginRoutes, { db: options.db });
28
- app.register(adminProviderRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController, proxyAgentFactory: options.proxyAgentFactory });
27
+ app.register(adminProviderRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController, proxyAgentFactory: options.proxyAgentFactory, connectivityChecker: options.connectivityChecker });
29
28
  app.register(adminMappingRoutes, { db: options.db });
30
29
  app.register(adminGroupRoutes, { db: options.db });
31
30
  app.register(adminScheduleRoutes, { db: options.db });
@@ -34,7 +33,7 @@ export const adminRoutes = (app, options, done) => {
34
33
  app.register(adminRouterKeyRoutes, { db: options.db });
35
34
  app.register(adminStatsRoutes, { db: options.db });
36
35
  app.register(adminMetricsRoutes, { db: options.db });
37
- app.register(adminProxyEnhancementRoutes, { db: options.db });
36
+ app.register(adminProxyEnhancementRoutes, { db: options.db, stateRegistry: options.stateRegistry });
38
37
  app.register(adminMonitorRoutes, { tracker: options.tracker });
39
38
  app.register(adminSettingsRoutes, { db: options.db, logsDir: options.logsDir });
40
39
  app.register(adminImportExportRoutes, { db: options.db, stateRegistry: options.stateRegistry, pluginRegistry: options.pluginRegistry });
@@ -46,7 +45,7 @@ export const adminRoutes = (app, options, done) => {
46
45
  app.register(adminTransformRuleRoutes, { db: options.db, pluginRegistry: options.pluginRegistry });
47
46
  // Pipeline hooks 查询
48
47
  app.get("/admin/api/pipeline/hooks", async () => {
49
- return { hooks: hookRegistry.getAll() };
48
+ return { hooks: options.stateRegistry.getPipelineHooks() };
50
49
  });
51
50
  done();
52
51
  };
@@ -1,9 +1,9 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getSchedulesByGroup, getAllSchedules, getScheduleById, createSchedule, updateSchedule, deleteSchedule, getAllMappingGroups, getAllProviders, } from "../db/index.js";
3
- import { getMappingGroupById, getProviderById } from "../db/index.js";
3
+ import { getMappingGroupById } from "../db/index.js";
4
4
  import { serializeProviders } from "./providers.js";
5
5
  import { getSetting } from "../db/settings.js";
6
- import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND } from "./constants.js";
6
+ import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND } from "../core/constants.js";
7
7
  import { API_CODE, apiError } from "./api-response.js";
8
8
  const CreateScheduleSchema = Type.Object({
9
9
  mapping_group_id: Type.String({ minLength: 1 }),
@@ -25,44 +25,7 @@ const UpdateScheduleSchema = Type.Object({
25
25
  concurrency_rule: Type.Optional(Type.String()),
26
26
  transform_rule: Type.Optional(Type.String()),
27
27
  });
28
- function validateMappingRule(db, ruleJson) {
29
- let rule;
30
- try {
31
- rule = JSON.parse(ruleJson);
32
- }
33
- catch {
34
- return "Invalid mapping_rule JSON";
35
- }
36
- if (typeof rule !== "object" || rule === null)
37
- return "Invalid mapping_rule";
38
- const r = rule;
39
- if (!Array.isArray(r.targets) || r.targets.length === 0) {
40
- return "mapping_rule.targets must be a non-empty array";
41
- }
42
- for (let i = 0; i < r.targets.length; i++) {
43
- const t = r.targets[i];
44
- if (!t.backend_model || !t.provider_id) {
45
- return `targets[${i}] missing backend_model or provider_id`;
46
- }
47
- const p = getProviderById(db, t.provider_id);
48
- if (!p)
49
- return `targets[${i}] provider_id '${t.provider_id}' not found`;
50
- const hasOverflowProvider = !!t.overflow_provider_id;
51
- const hasOverflowModel = !!t.overflow_model;
52
- if (hasOverflowProvider && !hasOverflowModel) {
53
- return `targets[${i}]: overflow_provider_id requires overflow_model`;
54
- }
55
- if (hasOverflowModel && !hasOverflowProvider) {
56
- return `targets[${i}]: overflow_model requires overflow_provider_id`;
57
- }
58
- if (hasOverflowProvider) {
59
- const op = getProviderById(db, t.overflow_provider_id);
60
- if (!op)
61
- return `targets[${i}]: overflow_provider_id '${t.overflow_provider_id}' not found`;
62
- }
63
- }
64
- return undefined;
65
- }
28
+ import { validateMappingRule } from "./utils.js";
66
29
  /** 解析 week JSON 为数字数组,失败返回 null */
67
30
  const MAX_WEEK_DAY = 6;
68
31
  function parseWeekSafe(weekJson) {
@@ -1,7 +1,7 @@
1
1
  import { statSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { getLogRetentionDays, setLogRetentionDays, getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, getSetting, getTokenEstimationEnabled, setTokenEstimationEnabled, getClientSessionHeaders, setClientSessionHeaders, getMetricsDetailDays, setMetricsDetailDays, } from "../db/settings.js";
4
- import { HTTP_BAD_REQUEST } from "./constants.js";
4
+ import { HTTP_BAD_REQUEST } from "../core/constants.js";
5
5
  import { API_CODE, apiError } from "./api-response.js";
6
6
  export const adminSettingsRoutes = (app, options, done) => {
7
7
  const { db, logsDir } = options;
@@ -3,7 +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 { isForwardedProtoHttps } from "../utils/cookie-secure.js";
6
- import { HTTP_BAD_REQUEST, HTTP_CONFLICT } from "./constants.js";
6
+ import { HTTP_BAD_REQUEST, HTTP_CONFLICT } from "../core/constants.js";
7
7
  import { API_CODE, apiError } from "./api-response.js";
8
8
  const CRYPTO_BYTES_LENGTH = 32;
9
9
  const MIN_PASSWORD_LENGTH = 6;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * admin 层共享校验工具函数和常量。
3
+ * 各 admin 文件统一从此模块导入,消除重复定义。
4
+ */
5
+ import type Database from "better-sqlite3";
6
+ /** Provider 名称合法字符:英文、数字、横线、下划线 */
7
+ export declare const PROVIDER_NAME_RE: RegExp;
8
+ /** 校验 base_url 是否为合法的 HTTP(S) URL */
9
+ export declare function isValidHttpUrl(str: string): boolean;
10
+ /** 校验 mapping_rule JSON 结构和引用完整性 */
11
+ export declare function validateMappingRule(db: Database.Database, ruleJson: string): string | undefined;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * admin 层共享校验工具函数和常量。
3
+ * 各 admin 文件统一从此模块导入,消除重复定义。
4
+ */
5
+ import { getProviderById } from "../db/index.js";
6
+ /** Provider 名称合法字符:英文、数字、横线、下划线 */
7
+ export const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
8
+ /** 校验 base_url 是否为合法的 HTTP(S) URL */
9
+ export function isValidHttpUrl(str) {
10
+ try {
11
+ const url = new URL(str);
12
+ return url.protocol === "http:" || url.protocol === "https:";
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ /** 校验 mapping_rule JSON 结构和引用完整性 */
19
+ export function validateMappingRule(db, ruleJson) {
20
+ let rule;
21
+ try {
22
+ rule = JSON.parse(ruleJson);
23
+ }
24
+ catch {
25
+ return "Invalid mapping_rule JSON";
26
+ }
27
+ if (typeof rule !== "object" || rule === null)
28
+ return "Invalid mapping_rule";
29
+ const r = rule;
30
+ if (!Array.isArray(r.targets) || r.targets.length === 0) {
31
+ return "mapping_rule.targets must be a non-empty array";
32
+ }
33
+ for (let i = 0; i < r.targets.length; i++) {
34
+ const t = r.targets[i];
35
+ if (!t.backend_model || !t.provider_id) {
36
+ return `targets[${i}] missing backend_model or provider_id`;
37
+ }
38
+ const p = getProviderById(db, t.provider_id);
39
+ if (!p)
40
+ return `targets[${i}] provider_id '${t.provider_id}' not found`;
41
+ const hasOverflowProvider = !!t.overflow_provider_id;
42
+ const hasOverflowModel = !!t.overflow_model;
43
+ if (hasOverflowProvider && !hasOverflowModel) {
44
+ return `targets[${i}]: overflow_provider_id requires overflow_model`;
45
+ }
46
+ if (hasOverflowModel && !hasOverflowProvider) {
47
+ return `targets[${i}]: overflow_model requires overflow_provider_id`;
48
+ }
49
+ if (hasOverflowProvider) {
50
+ const op = getProviderById(db, t.overflow_provider_id);
51
+ if (!op) {
52
+ return `targets[${i}]: overflow_provider_id '${t.overflow_provider_id}' not found`;
53
+ }
54
+ }
55
+ }
56
+ return undefined;
57
+ }
@@ -0,0 +1,12 @@
1
+ export interface RetryMatcher {
2
+ test: (statusCode: number, body: string) => boolean;
3
+ }
4
+ /**
5
+ * 判断一条日志是否需要保留全文详情到 DB。
6
+ * - hasFileWriter=false 时保守保留全文(避免数据丢失)
7
+ * - status >= 400 → 保留
8
+ * - matcher 为 null → 保守保留
9
+ * - matcher 命中 → 保留
10
+ * - 否则 → 只存摘要(文件已有全文备份)
11
+ */
12
+ export declare function shouldPreserveDetail(statusCode: number | null, responseBody: string | null, matcher: RetryMatcher | null, hasFileWriter?: boolean): boolean;
@@ -0,0 +1,21 @@
1
+ // src/core/log-detail-policy.ts
2
+ const HTTP_ERROR_THRESHOLD = 400;
3
+ /**
4
+ * 判断一条日志是否需要保留全文详情到 DB。
5
+ * - hasFileWriter=false 时保守保留全文(避免数据丢失)
6
+ * - status >= 400 → 保留
7
+ * - matcher 为 null → 保守保留
8
+ * - matcher 命中 → 保留
9
+ * - 否则 → 只存摘要(文件已有全文备份)
10
+ */
11
+ export function shouldPreserveDetail(statusCode, responseBody, matcher, hasFileWriter = true) {
12
+ if (!hasFileWriter)
13
+ return true;
14
+ if (statusCode !== null && statusCode >= HTTP_ERROR_THRESHOLD)
15
+ return true;
16
+ if (!matcher)
17
+ return true;
18
+ if (responseBody && matcher.test(statusCode ?? 0, responseBody))
19
+ return true;
20
+ return false;
21
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Provider 连通性检查接口。
3
+ * admin 层通过此接口检查上游 provider 的模型列表,解耦对 proxy 层的直接依赖。
4
+ */
5
+ import type { RawHeaders } from "./types.js";
6
+ export interface ProviderConnectivityCheckResult {
7
+ statusCode: number;
8
+ body: string;
9
+ }
10
+ export interface ProviderConnectivityChecker {
11
+ /**
12
+ * 向上游 provider 发起 GET 请求获取模型列表。
13
+ * @param backend 包含 base_url 的后端信息
14
+ * @param apiKey API 密钥
15
+ * @param clientHeaders 客户端原始请求头
16
+ * @param upstreamPath 上游路径(如 /v1/models)
17
+ * @param apiType API 类型,决定 header 构建方式
18
+ */
19
+ fetchModels(backend: {
20
+ base_url: string;
21
+ }, apiKey: string, clientHeaders: RawHeaders, upstreamPath: string, apiType: "openai" | "openai-responses" | "anthropic"): Promise<ProviderConnectivityCheckResult>;
22
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Provider 连通性检查接口。
3
+ * admin 层通过此接口检查上游 provider 的模型列表,解耦对 proxy 层的直接依赖。
4
+ */
5
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Proxy Agent 缓存失效接口。
3
+ * admin 层通过此接口在 provider 更新/删除时清除对应的 proxy agent 缓存,
4
+ * 解耦对 proxy 层的直接类型依赖。
5
+ */
6
+ export interface IProxyAgentInvalidator {
7
+ /** 使指定 provider 的代理 agent 缓存失效 */
8
+ invalidate(providerId: string): void;
9
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Proxy Agent 缓存失效接口。
3
+ * admin 层通过此接口在 provider 更新/删除时清除对应的 proxy agent 缓存,
4
+ * 解耦对 proxy 层的直接类型依赖。
5
+ */
6
+ export {};
@@ -29,4 +29,11 @@ export interface StateRegistry {
29
29
  getAdaptiveStatus(providerId: string): import("./concurrency/types.js").AdaptiveState | undefined;
30
30
  /** 从 DB 重新读取所有 provider 配置,重建信号量/adaptive/tracker 缓存(导入配置后调用) */
31
31
  reinitializeProviders(): void;
32
+ /** 清除代理增强配置缓存(enhancement-config) */
33
+ clearEnhancementCache(): void;
34
+ /** 获取已注册的 pipeline hooks(按 phase 分组) */
35
+ getPipelineHooks(): Record<string, Array<{
36
+ name: string;
37
+ priority: number;
38
+ }>>;
32
39
  }
package/dist/db/logs.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import Database from "better-sqlite3";
2
2
  import type { LogFileWriter } from "../storage/log-file-writer.js";
3
- import { type RetryMatcher } from "../proxy/log-detail-policy.js";
3
+ import { type RetryMatcher } from "../core/log-detail-policy.js";
4
4
  /** 从 client_request JSON 中提取 thinking_level,按 api_type 分支处理 */
5
5
  export declare function extractThinkingLevel(apiType: string, clientRequest: string | null): string;
6
6
  export interface RequestLog {
package/dist/db/logs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { shouldPreserveDetail } from "../proxy/log-detail-policy.js";
1
+ import { shouldPreserveDetail } from "../core/log-detail-policy.js";
2
2
  import { getCachedStmt } from "./helpers.js";
3
3
  /** 从 client_request JSON 中提取 thinking_level,按 api_type 分支处理 */
4
4
  export function extractThinkingLevel(apiType, clientRequest) {
package/dist/index.js CHANGED
@@ -42,6 +42,9 @@ import fastifyStatic from "@fastify/static";
42
42
  import { ServiceContainer, SERVICE_KEYS } from "./core/container.js";
43
43
  import { LogFileWriter } from "./storage/log-file-writer.js";
44
44
  import { ProxyAgentFactory } from "./proxy/transport/proxy-agent.js";
45
+ import { ProxyConnectivityChecker } from "./proxy/transport/provider-connectivity.js";
46
+ import { hookRegistry } from "./proxy/pipeline/hook-registry.js";
47
+ import { clearEnhancementConfigCache } from "./proxy/routing/enhancement-config.js";
45
48
  import { registerBuiltinHooks } from "./proxy/pipeline/register-hooks.js";
46
49
  import { scheduleLogFileMaintenance } from "./storage/log-file-compressor.js";
47
50
  import { getDetailLogEnabled, getLogFileRetentionDays } from "./db/settings.js";
@@ -274,6 +277,7 @@ export async function buildApp(options) {
274
277
  container.register(SERVICE_KEYS.formatRegistry, () => formatRegistry);
275
278
  // 注册 ProxyAgentFactory
276
279
  container.register(SERVICE_KEYS.proxyAgentFactory, () => new ProxyAgentFactory());
280
+ const connectivityChecker = new ProxyConnectivityChecker();
277
281
  // 从容器解析所有服务
278
282
  const matcher = container.resolve(SERVICE_KEYS.matcher);
279
283
  const semaphoreManager = container.resolve(SERVICE_KEYS.semaphoreManager);
@@ -318,11 +322,13 @@ export async function buildApp(options) {
318
322
  adaptiveController.removeAll();
319
323
  initializeProviderState(db, semaphoreManager, adaptiveController, tracker);
320
324
  },
325
+ clearEnhancementCache: () => clearEnhancementConfigCache(),
326
+ getPipelineHooks: () => hookRegistry.getAll(),
321
327
  };
322
328
  // Late-bound close ref — close 函数在 adminRoutes 注册之后才定义,
323
329
  // 但 restart API 需要在运行时调用它
324
330
  const closeRef = { fn: async () => { } };
325
- app.register(adminRoutes, { db, stateRegistry, tracker, adaptiveController, logFileWriter, logsDir, closeFn: () => closeRef.fn(), pluginRegistry, proxyAgentFactory });
331
+ app.register(adminRoutes, { db, stateRegistry, tracker, adaptiveController, logFileWriter, logsDir, closeFn: () => closeRef.fn(), pluginRegistry, proxyAgentFactory, connectivityChecker });
326
332
  // 前端静态文件服务(生产环境)
327
333
  const frontendDist = path.resolve(process.env.FRONTEND_DIST || path.join(__dirname, "../frontend-dist"));
328
334
  if (existsSync(frontendDist)) {
@@ -1,12 +1 @@
1
- export interface RetryMatcher {
2
- test: (statusCode: number, body: string) => boolean;
3
- }
4
- /**
5
- * 判断一条日志是否需要保留全文详情到 DB。
6
- * - hasFileWriter=false 时保守保留全文(避免数据丢失)
7
- * - status >= 400 → 保留
8
- * - matcher 为 null → 保守保留
9
- * - matcher 命中 → 保留
10
- * - 否则 → 只存摘要(文件已有全文备份)
11
- */
12
- export declare function shouldPreserveDetail(statusCode: number | null, responseBody: string | null, matcher: RetryMatcher | null, hasFileWriter?: boolean): boolean;
1
+ export { shouldPreserveDetail, type RetryMatcher } from "../core/log-detail-policy.js";
@@ -1,21 +1,3 @@
1
1
  // src/proxy/log-detail-policy.ts
2
- const HTTP_ERROR_THRESHOLD = 400;
3
- /**
4
- * 判断一条日志是否需要保留全文详情到 DB。
5
- * - hasFileWriter=false 时保守保留全文(避免数据丢失)
6
- * - status >= 400 → 保留
7
- * - matcher 为 null → 保守保留
8
- * - matcher 命中 → 保留
9
- * - 否则 → 只存摘要(文件已有全文备份)
10
- */
11
- export function shouldPreserveDetail(statusCode, responseBody, matcher, hasFileWriter = true) {
12
- if (!hasFileWriter)
13
- return true;
14
- if (statusCode !== null && statusCode >= HTTP_ERROR_THRESHOLD)
15
- return true;
16
- if (!matcher)
17
- return true;
18
- if (responseBody && matcher.test(statusCode ?? 0, responseBody))
19
- return true;
20
- return false;
21
- }
2
+ // 策略逻辑已提升到 core 层,此文件保留 re-export 兼容
3
+ export { shouldPreserveDetail } from "../core/log-detail-policy.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ProviderConnectivityChecker 的 proxy 层实现。
3
+ * 组合 proxy 层的 callGet + buildUpstreamHeaders,通过接口暴露给 admin 层。
4
+ */
5
+ import type { ProviderConnectivityChecker, ProviderConnectivityCheckResult } from "../../core/provider-connectivity.js";
6
+ import type { RawHeaders } from "../../core/types.js";
7
+ export declare class ProxyConnectivityChecker implements ProviderConnectivityChecker {
8
+ fetchModels(backend: {
9
+ base_url: string;
10
+ }, apiKey: string, clientHeaders: RawHeaders, upstreamPath: string, apiType: "openai" | "openai-responses" | "anthropic"): Promise<ProviderConnectivityCheckResult>;
11
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ProviderConnectivityChecker 的 proxy 层实现。
3
+ * 组合 proxy 层的 callGet + buildUpstreamHeaders,通过接口暴露给 admin 层。
4
+ */
5
+ import { callGet } from "./http.js";
6
+ import { buildUpstreamHeaders } from "../proxy-core.js";
7
+ export class ProxyConnectivityChecker {
8
+ fetchModels(backend, apiKey, clientHeaders, upstreamPath, apiType) {
9
+ return callGet(backend, apiKey, clientHeaders, upstreamPath, (cliHdrs, key) => buildUpstreamHeaders(cliHdrs, key, undefined, apiType));
10
+ }
11
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Agent } from "http";
2
+ import type { IProxyAgentInvalidator } from "../../core/proxy-agent-types.js";
2
3
  export interface ProxyConfig {
3
4
  id: string;
4
5
  proxy_type: string | null;
@@ -6,7 +7,7 @@ export interface ProxyConfig {
6
7
  proxy_username: string | null;
7
8
  proxy_password: string | null;
8
9
  }
9
- export declare class ProxyAgentFactory {
10
+ export declare class ProxyAgentFactory implements IProxyAgentInvalidator {
10
11
  private readonly cache;
11
12
  private keepAliveHttpAgent;
12
13
  private keepAliveHttpsAgent;
@@ -1 +1 @@
1
- import{$ as e,Qt as t,at as n,et as r,jt as i,n as a,nt as o,ot as s,qt as c,vt as l,xt as u}from"./button-DpPvjx0f.js";import{dt as d,ft as f,r as p}from"./index-Di7f6Crg.js";import{t as m}from"./Card-C5jCkE1m.js";import{t as h}from"./CardContent-Dozca7Pu.js";var g={class:`min-h-screen flex items-center justify-center bg-background relative`},_={class:`text-center mb-6`},v={class:`text-sm text-muted-foreground mt-1`},y=s({__name:`AuthLayout`,props:{subtitle:{}},setup(s){let{isDark:y,toggleTheme:b}=p();return(p,x)=>(l(),o(`div`,g,[n(c(a),{variant:`ghost`,size:`icon`,class:`absolute top-4 right-4 text-muted-foreground hover:text-foreground`,onClick:c(b)},{default:i(()=>[c(y)?(l(),r(c(d),{key:1,class:`w-4 h-4`})):(l(),r(c(f),{key:0,class:`w-4 h-4`}))]),_:1},8,[`onClick`]),n(c(m),{class:`w-full max-w-sm shadow-lg`},{default:i(()=>[n(c(h),{class:`pt-6`},{default:i(()=>[e(`div`,_,[x[0]||=e(`div`,{class:`w-12 h-12 bg-primary rounded-lg mx-auto mb-3 flex items-center justify-center`},[e(`svg`,{class:`w-7 h-7 text-white`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[e(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z`})])],-1),x[1]||=e(`h1`,{class:`text-xl font-semibold text-foreground`},` LLM Simple Router `,-1),e(`p`,v,t(s.subtitle),1)]),u(p.$slots,`default`)]),_:3})]),_:3})]))}});export{y as t};
1
+ import{$ as e,Qt as t,at as n,et as r,jt as i,n as a,nt as o,ot as s,qt as c,vt as l,xt as u}from"./button-mHNAa2aF.js";import{dt as d,ft as f,r as p}from"./index-Ddg5hheZ.js";import{t as m}from"./Card-CrAceTFb.js";import{t as h}from"./CardContent-otK_vTWx.js";var g={class:`min-h-screen flex items-center justify-center bg-background relative`},_={class:`text-center mb-6`},v={class:`text-sm text-muted-foreground mt-1`},y=s({__name:`AuthLayout`,props:{subtitle:{}},setup(s){let{isDark:y,toggleTheme:b}=p();return(p,x)=>(l(),o(`div`,g,[n(c(a),{variant:`ghost`,size:`icon`,class:`absolute top-4 right-4 text-muted-foreground hover:text-foreground`,onClick:c(b)},{default:i(()=>[c(y)?(l(),r(c(d),{key:1,class:`w-4 h-4`})):(l(),r(c(f),{key:0,class:`w-4 h-4`}))]),_:1},8,[`onClick`]),n(c(m),{class:`w-full max-w-sm shadow-lg`},{default:i(()=>[n(c(h),{class:`pt-6`},{default:i(()=>[e(`div`,_,[x[0]||=e(`div`,{class:`w-12 h-12 bg-primary rounded-lg mx-auto mb-3 flex items-center justify-center`},[e(`svg`,{class:`w-7 h-7 text-white`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[e(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z`})])],-1),x[1]||=e(`h1`,{class:`text-xl font-semibold text-foreground`},` LLM Simple Router `,-1),e(`p`,v,t(s.subtitle),1)]),u(p.$slots,`default`)]),_:3})]),_:3})]))}});export{y as t};