llm-simple-router 0.8.2 → 0.9.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 (120) hide show
  1. package/config/recommended-providers.json +33 -9
  2. package/config/recommended-retry-rules.json +9 -8
  3. package/dist/admin/providers.js +11 -9
  4. package/dist/admin/quick-setup.d.ts +13 -0
  5. package/dist/admin/quick-setup.js +169 -0
  6. package/dist/admin/recommended.js +5 -1
  7. package/dist/admin/routes.js +2 -0
  8. package/dist/config/model-context.d.ts +8 -2
  9. package/dist/config/model-context.js +17 -5
  10. package/dist/config/recommended.d.ts +1 -0
  11. package/dist/config/recommended.js +5 -9
  12. package/dist/proxy/handler/proxy-handler.js +8 -2
  13. package/dist/proxy/patch/index.d.ts +3 -0
  14. package/dist/proxy/patch/index.js +28 -0
  15. package/frontend-dist/assets/CardContent-F3K9pZNP.js +1 -0
  16. package/frontend-dist/assets/CardTitle-13anASyk.js +1 -0
  17. package/frontend-dist/assets/CascadingModelSelect-BmW89GUP.js +1 -0
  18. package/frontend-dist/assets/Checkbox-C2oSHNgP.js +1 -0
  19. package/frontend-dist/assets/CollapsibleContent-CdeCo0Ko.js +1 -0
  20. package/frontend-dist/assets/CollapsibleTrigger-CMd4wTNY.js +1 -0
  21. package/frontend-dist/assets/Collection-BulopTxo.js +1 -0
  22. package/frontend-dist/assets/Dashboard-BahJSTKV.js +3 -0
  23. package/frontend-dist/assets/DialogTitle-CnqbO2hx.js +1 -0
  24. package/frontend-dist/assets/{Input-CAnKUBBK.js → Input-RyuwzbNx.js} +1 -1
  25. package/frontend-dist/assets/Label-73u_Os4X.js +1 -0
  26. package/frontend-dist/assets/Login-CoQSrVLo.js +1 -0
  27. package/frontend-dist/assets/Logs-C2b6MPXL.js +1 -0
  28. package/frontend-dist/assets/MappingList-m2ebUmJ9.js +1 -0
  29. package/frontend-dist/assets/ModelCard-B0pjEq6W.js +1 -0
  30. package/frontend-dist/assets/ModelMappings-BazKS9T4.js +1 -0
  31. package/frontend-dist/assets/Monitor-8B_tm1NO.js +1 -0
  32. package/frontend-dist/assets/{PopoverTrigger-Bj65uUbv.js → PopoverTrigger-DSmA2dE4.js} +1 -1
  33. package/frontend-dist/assets/{PopperContent-gzzf1XHe.js → PopperContent-Bd_mpt_D.js} +1 -1
  34. package/frontend-dist/assets/Providers-TI83sF2T.js +1 -0
  35. package/frontend-dist/assets/ProxyEnhancement-CWLh-YlM.js +5 -0
  36. package/frontend-dist/assets/QuickSetup-DUZNdIvp.js +1 -0
  37. package/frontend-dist/assets/RetryRules-CzhCNQ0R.js +1 -0
  38. package/frontend-dist/assets/RouterKeys-B_C-Wp_I.js +1 -0
  39. package/frontend-dist/assets/RovingFocusItem-DwGTruuB.js +1 -0
  40. package/frontend-dist/assets/Schedules-BMB6RX9e.js +1 -0
  41. package/frontend-dist/assets/SelectValue-DRc1qira.js +1 -0
  42. package/frontend-dist/assets/Settings-Ck8CoUJC.js +6 -0
  43. package/frontend-dist/assets/Setup-dwKkHGrB.js +1 -0
  44. package/frontend-dist/assets/Switch-BE8DAylK.js +1 -0
  45. package/frontend-dist/assets/TableHeader-BqYT-eO-.js +1 -0
  46. package/frontend-dist/assets/Teleport-CLw1Jxrb.js +3 -0
  47. package/frontend-dist/assets/TooltipTrigger-bqCyq9MU.js +1 -0
  48. package/frontend-dist/assets/UnifiedRequestDialog-BDNR1wzi.js +3 -0
  49. package/frontend-dist/assets/UnifiedRequestDialog-DmpjVK9n.css +1 -0
  50. package/frontend-dist/assets/VisuallyHidden-BpDuyh8-.js +1 -0
  51. package/frontend-dist/assets/VisuallyHiddenInput-CCL5ykZW.js +1 -0
  52. package/frontend-dist/assets/alert-dialog-gprnWn1b.js +1 -0
  53. package/frontend-dist/assets/badge-CpT5q-jI.js +1 -0
  54. package/frontend-dist/assets/{button-Ul8WlrM5.js → button-zud8Qspb.js} +7 -7
  55. package/frontend-dist/assets/check-CRv7NpkT.js +1 -0
  56. package/frontend-dist/assets/constants-ncbNnOLM.js +1 -0
  57. package/frontend-dist/assets/copy-CIHn6HDL.js +1 -0
  58. package/frontend-dist/assets/dialog-Da8YFS7g.js +1 -0
  59. package/frontend-dist/assets/file-text-LfP0_JRK.js +1 -0
  60. package/frontend-dist/assets/index-BfXK7SYr.js +1 -0
  61. package/frontend-dist/assets/index-CDtb1WVq.css +1 -0
  62. package/frontend-dist/assets/lib-xfvPneK8.js +1 -0
  63. package/frontend-dist/assets/loader-circle-D8BaqxEc.js +1 -0
  64. package/frontend-dist/assets/sun-n4cC12ho.js +1 -0
  65. package/frontend-dist/assets/trash-2-oDWBOuqK.js +1 -0
  66. package/frontend-dist/assets/{useClipboard-BmmsNSGV.js → useClipboard-C2i7YvJ-.js} +1 -1
  67. package/frontend-dist/assets/{useFocusGuards-A-9V2Y-b.js → useFocusGuards-DORIgNd9.js} +1 -1
  68. package/frontend-dist/assets/useFormControl-OyxyVR_M.js +1 -0
  69. package/frontend-dist/assets/{useLogRetention-BfnBFZ5K.js → useLogRetention-DE7zYGFK.js} +1 -1
  70. package/frontend-dist/assets/useNonce-D_84NiFG.js +1 -0
  71. package/frontend-dist/assets/useTheme-BFhy-DAX.js +1 -0
  72. package/frontend-dist/assets/x-BN5AHIVk.js +1 -0
  73. package/frontend-dist/index.html +22 -20
  74. package/package.json +1 -1
  75. package/frontend-dist/assets/CardContent-BVMQ2_pg.js +0 -1
  76. package/frontend-dist/assets/CardTitle-GLv7QyIY.js +0 -1
  77. package/frontend-dist/assets/CascadingModelSelect-CBhqKFDX.js +0 -1
  78. package/frontend-dist/assets/Checkbox-HPVDmEdV.js +0 -1
  79. package/frontend-dist/assets/CollapsibleTrigger-DhxD9tpM.js +0 -1
  80. package/frontend-dist/assets/Collection-BRt7YxN8.js +0 -1
  81. package/frontend-dist/assets/Dashboard-D1Ys8Zog.js +0 -3
  82. package/frontend-dist/assets/DialogTitle-23q73lwF.js +0 -1
  83. package/frontend-dist/assets/Label-DWdYtVMI.js +0 -1
  84. package/frontend-dist/assets/Login-w5WFOinP.js +0 -1
  85. package/frontend-dist/assets/Logs-C1F1ZmWF.js +0 -1
  86. package/frontend-dist/assets/ModelMappings-BzmecWEH.js +0 -1
  87. package/frontend-dist/assets/Monitor-DrAZFTKR.js +0 -1
  88. package/frontend-dist/assets/Providers-DSgf4mb6.js +0 -1
  89. package/frontend-dist/assets/ProxyEnhancement-Bb1cCP6d.js +0 -5
  90. package/frontend-dist/assets/RetryRules-BwPfEZtm.js +0 -1
  91. package/frontend-dist/assets/RouterKeys-CzTSq1Mx.js +0 -1
  92. package/frontend-dist/assets/RovingFocusItem-CXM_Yfkm.js +0 -1
  93. package/frontend-dist/assets/Schedules-DVilCXrC.js +0 -1
  94. package/frontend-dist/assets/SelectValue-C0-LzGQY.js +0 -1
  95. package/frontend-dist/assets/Settings-Bpk53zVX.js +0 -6
  96. package/frontend-dist/assets/Setup-Dn7EgC49.js +0 -1
  97. package/frontend-dist/assets/Switch-BO8Ooae6.js +0 -1
  98. package/frontend-dist/assets/TableHeader-Bded9VTC.js +0 -1
  99. package/frontend-dist/assets/TabsTrigger-BzKMi9AF.js +0 -1
  100. package/frontend-dist/assets/Teleport-DizRK5O3.js +0 -3
  101. package/frontend-dist/assets/TooltipTrigger-EiIy2zn8.js +0 -1
  102. package/frontend-dist/assets/UnifiedRequestDialog-BABsTaGb.js +0 -3
  103. package/frontend-dist/assets/UnifiedRequestDialog-BjEigSaR.css +0 -1
  104. package/frontend-dist/assets/VisuallyHidden-5AozJQza.js +0 -1
  105. package/frontend-dist/assets/VisuallyHiddenInput-DdiZrV2i.js +0 -1
  106. package/frontend-dist/assets/alert-dialog-DlKUuTPe.js +0 -1
  107. package/frontend-dist/assets/arrow-down-CxWKmZ2I.js +0 -1
  108. package/frontend-dist/assets/badge-9KJEMa53.js +0 -1
  109. package/frontend-dist/assets/check-7ahK--N4.js +0 -1
  110. package/frontend-dist/assets/constants-D_0jiLjw.js +0 -1
  111. package/frontend-dist/assets/copy-DzU2pAMG.js +0 -1
  112. package/frontend-dist/assets/dialog-B9j-FMrd.js +0 -1
  113. package/frontend-dist/assets/file-text-Bj3ZIo-E.js +0 -1
  114. package/frontend-dist/assets/index-Bz_ZaXNn.css +0 -1
  115. package/frontend-dist/assets/index-MedWZMHB.js +0 -1
  116. package/frontend-dist/assets/lib-Hhs3NqfD.js +0 -1
  117. package/frontend-dist/assets/loader-circle-5TJUukEe.js +0 -1
  118. package/frontend-dist/assets/useFormControl-DEO19lRe.js +0 -1
  119. package/frontend-dist/assets/useNonce-BfwUJ1Ci.js +0 -1
  120. package/frontend-dist/assets/x-Cfopt3QL.js +0 -1
@@ -7,14 +7,20 @@
7
7
  "presetName": "deepseek",
8
8
  "apiType": "anthropic",
9
9
  "baseUrl": "https://api.deepseek.com/anthropic",
10
- "models": ["deepseek-chat", "deepseek-reasoner"]
10
+ "models": [
11
+ "deepseek-v4-flash",
12
+ "deepseek-v4-pro"
13
+ ]
11
14
  },
12
15
  {
13
16
  "plan": "OpenAI",
14
17
  "presetName": "deepseek-openai",
15
18
  "apiType": "openai",
16
19
  "baseUrl": "https://api.deepseek.com",
17
- "models": ["deepseek-chat", "deepseek-reasoner"]
20
+ "models": [
21
+ "deepseek-v4-flash",
22
+ "deepseek-v4-pro"
23
+ ]
18
24
  }
19
25
  ]
20
26
  },
@@ -86,7 +92,12 @@
86
92
  "presetName": "zhipu-coding-plan",
87
93
  "apiType": "anthropic",
88
94
  "baseUrl": "https://open.bigmodel.cn/api/anthropic",
89
- "models": ["glm-5.1", "glm-5", "glm-4.7", "glm-4.5-air"]
95
+ "models": [
96
+ "glm-5.1",
97
+ "glm-5",
98
+ "glm-4.7",
99
+ "glm-4.5-air"
100
+ ]
90
101
  },
91
102
  {
92
103
  "plan": "API",
@@ -105,14 +116,17 @@
105
116
  ]
106
117
  },
107
118
  {
108
- "group": "KIMI",
119
+ "group": "月之暗面",
109
120
  "presets": [
110
121
  {
111
122
  "plan": "Coding Plan",
112
123
  "presetName": "kimi-coding-plan",
113
124
  "apiType": "anthropic",
114
125
  "baseUrl": "https://api.kimi.com/coding",
115
- "models": ["kimi-for-coding", "kimi-k2.5"]
126
+ "models": [
127
+ "kimi-for-coding",
128
+ "kimi-k2.5"
129
+ ]
116
130
  },
117
131
  {
118
132
  "plan": "API",
@@ -137,7 +151,9 @@
137
151
  "presetName": "minimax-token-plan",
138
152
  "apiType": "anthropic",
139
153
  "baseUrl": "https://api.minimaxi.com/anthropic",
140
- "models": ["MiniMax-M2.7"]
154
+ "models": [
155
+ "MiniMax-M2.7"
156
+ ]
141
157
  },
142
158
  {
143
159
  "plan": "API",
@@ -258,7 +274,12 @@
258
274
  "presetName": "opencode-go-anthropic",
259
275
  "apiType": "anthropic",
260
276
  "baseUrl": "https://opencode.ai/zen/go/v1/messages",
261
- "models": ["deepseek-v4-pro", "deepseek-v4-flash", "minimax-m2.7", "minimax-m2.5"]
277
+ "models": [
278
+ "deepseek-v4-pro",
279
+ "deepseek-v4-flash",
280
+ "minimax-m2.7",
281
+ "minimax-m2.5"
282
+ ]
262
283
  }
263
284
  ]
264
285
  },
@@ -270,7 +291,10 @@
270
291
  "presetName": "stepfun-step-plan",
271
292
  "apiType": "anthropic",
272
293
  "baseUrl": "https://api.stepfun.com/step_plan",
273
- "models": ["step-3.5-flash-2603", "step-3.5-flash"]
294
+ "models": [
295
+ "step-3.5-flash-2603",
296
+ "step-3.5-flash"
297
+ ]
274
298
  },
275
299
  {
276
300
  "plan": "API",
@@ -288,4 +312,4 @@
288
312
  }
289
313
  ]
290
314
  }
291
- ]
315
+ ]
@@ -1,10 +1,11 @@
1
1
  [
2
- { "name": "429 Too Many Requests", "status_code": 429, "body_pattern": ".*", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
3
- { "name": "503 Service Unavailable", "status_code": 503, "body_pattern": ".*", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
4
- { "name": "ZAI 网络错误 (code 1234)", "status_code": 400, "body_pattern": "\"type\"\\s*:\\s*\"error\".*\"code\"\\s*:\\s*\"1234\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
5
- { "name": "ZAI 临时不可用", "status_code": 400, "body_pattern": "\"type\"\\s*:\\s*\"error\".*请稍后重试", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
6
- { "name": "ZAI 操作失败 (code 500)", "status_code": 400, "body_pattern": "\"type\"\\s*:\\s*\"error\".*\"code\"\\s*:\\s*\"500\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
7
- { "name": "ZAI 速率限制 (HTTP 200, code 1302)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1302\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
8
- { "name": "ZAI SSE 错误 (HTTP 200, code 500)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"500\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
9
- { "name": "ZAI SSE 错误 (HTTP 200, code 1234)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1234\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 }
2
+ { "name": "429 Too Many Requests", "status_code": 429, "body_pattern": ".*", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": [] },
3
+ { "name": "503 Service Unavailable", "status_code": 503, "body_pattern": ".*", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": [] },
4
+ { "name": "ZAI 网络错误 (code 1234)", "status_code": 400, "body_pattern": "\"type\"\\s*:\\s*\"error\".*\"code\"\\s*:\\s*\"1234\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
5
+ { "name": "ZAI 临时不可用", "status_code": 400, "body_pattern": "\"type\"\\s*:\\s*\"error\".*请稍后重试", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
6
+ { "name": "ZAI 操作失败 (code 500)", "status_code": 400, "body_pattern": "\"type\"\\s*:\\s*\"error\".*\"code\"\\s*:\\s*\"500\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
7
+ { "name": "ZAI 速率限制 (HTTP 200, code 1302)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1302\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
8
+ { "name": "ZAI SSE 错误 (HTTP 200, code 500)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"500\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
9
+ { "name": "ZAI SSE 错误 (HTTP 200, code 1234)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1234\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
10
+ { "name": "KIMI 401 认证错误", "status_code": 401, "body_pattern": ".*authentication_error.*", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 3, "max_delay_ms": 60000, "providers": ["月之暗面"] }
10
11
  ]
@@ -58,9 +58,11 @@ function cascadeProviderDisable(db, providerId) {
58
58
  return result;
59
59
  }
60
60
  function extractModelOverrides(models) {
61
- const names = models.map(m => typeof m === "string" ? m : m.name);
61
+ const entries = models.map(m => typeof m === "string"
62
+ ? { name: m, patches: [] }
63
+ : { name: m.name, context_window: m.context_window, patches: m.patches ?? [] });
62
64
  const overrides = models.filter((m) => typeof m !== "string" && m.context_window != null);
63
- return { names, overrides };
65
+ return { entries, overrides };
64
66
  }
65
67
  const API_KEY_PREVIEW_PREFIX_LEN = 4;
66
68
  const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
@@ -71,7 +73,7 @@ const CreateProviderSchema = Type.Object({
71
73
  api_key: Type.String({ minLength: 1 }),
72
74
  models: Type.Optional(Type.Array(Type.Union([
73
75
  Type.String(),
74
- Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()) })
76
+ Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())) })
75
77
  ]))),
76
78
  is_active: Type.Optional(Type.Number()),
77
79
  max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
@@ -86,7 +88,7 @@ const UpdateProviderSchema = Type.Object({
86
88
  api_key: Type.Optional(Type.String({ minLength: 1 })),
87
89
  models: Type.Optional(Type.Array(Type.Union([
88
90
  Type.String(),
89
- Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()) })
91
+ Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())) })
90
92
  ]))),
91
93
  is_active: Type.Optional(Type.Number()),
92
94
  max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
@@ -100,7 +102,7 @@ export const adminProviderRoutes = (app, options, done) => {
100
102
  const encryptionKey = getSetting(db, "encryption_key");
101
103
  const providers = getAllProviders(db);
102
104
  return reply.send(providers.map((s) => {
103
- const modelNames = parseModels(s.models || "[]");
105
+ const modelEntries = parseModels(s.models || "[]");
104
106
  const overrides = new Map(getModelInfoForProvider(db, s.id).map(m => [m.model_name, m.context_window]));
105
107
  return {
106
108
  id: s.id,
@@ -108,7 +110,7 @@ export const adminProviderRoutes = (app, options, done) => {
108
110
  api_type: s.api_type,
109
111
  base_url: s.base_url,
110
112
  api_key: s.api_key ? decrypt(s.api_key, encryptionKey) : "",
111
- models: buildModelInfoList(modelNames, overrides),
113
+ models: buildModelInfoList(modelEntries, overrides),
112
114
  is_active: s.is_active,
113
115
  max_concurrency: s.max_concurrency,
114
116
  queue_timeout_ms: s.queue_timeout_ms,
@@ -130,7 +132,7 @@ export const adminProviderRoutes = (app, options, done) => {
130
132
  return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, `Provider 名称 '${body.name}' 已存在`));
131
133
  }
132
134
  const encryptedKey = encrypt(body.api_key, getSetting(db, "encryption_key"));
133
- const { names: normalizedModels, overrides: contextOverrides } = extractModelOverrides((body.models ?? []));
135
+ const { entries: normalizedModels, overrides: contextOverrides } = extractModelOverrides((body.models ?? []));
134
136
  const isAdaptiveEnabled = body.adaptive_enabled ?? 0;
135
137
  const id = createProvider(db, {
136
138
  name: body.name,
@@ -190,8 +192,8 @@ export const adminProviderRoutes = (app, options, done) => {
190
192
  if (body.is_active !== undefined)
191
193
  fields.is_active = body.is_active;
192
194
  if (body.models !== undefined) {
193
- const { names, overrides } = extractModelOverrides(body.models);
194
- fields.models = JSON.stringify(names);
195
+ const { entries, overrides } = extractModelOverrides(body.models);
196
+ fields.models = JSON.stringify(entries);
195
197
  if (overrides.length > 0) {
196
198
  setModelInfoForProvider(db, id, overrides.map(o => ({ model_name: o.name, context_window: o.context_window })));
197
199
  }
@@ -0,0 +1,13 @@
1
+ import { FastifyPluginCallback } from "fastify";
2
+ import Database from "better-sqlite3";
3
+ import type { StateRegistry } from "../core/registry.js";
4
+ import type { RequestTracker } from "../monitor/request-tracker.js";
5
+ import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
6
+ interface QuickSetupRoutesOptions {
7
+ db: Database.Database;
8
+ stateRegistry?: StateRegistry;
9
+ tracker?: RequestTracker;
10
+ adaptiveController?: AdaptiveConcurrencyController;
11
+ }
12
+ export declare const adminQuickSetupRoutes: FastifyPluginCallback<QuickSetupRoutesOptions>;
13
+ export {};
@@ -0,0 +1,169 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { createProvider } from "../db/providers.js";
3
+ import { createMappingGroup, updateMappingGroup } from "../db/mappings.js";
4
+ import { createRetryRule } from "../db/retry-rules.js";
5
+ import { upsertTransformRule } from "../db/transform-rules.js";
6
+ import { encrypt } from "../utils/crypto.js";
7
+ import { getSetting } from "../db/settings.js";
8
+ import { HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_CONFLICT } from "./constants.js";
9
+ import { API_CODE, apiError } from "./api-response.js";
10
+ import { PROVIDER_CONCURRENCY_DEFAULTS } from "../db/providers.js";
11
+ const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
12
+ const API_KEY_PREVIEW_MIN_LENGTH = 8;
13
+ const API_KEY_PREVIEW_PREFIX_LEN = 4;
14
+ const QuickSetupProviderSchema = Type.Object({
15
+ name: Type.String({ minLength: 1 }),
16
+ api_type: Type.Union([Type.Literal("openai"), Type.Literal("anthropic")]),
17
+ base_url: Type.String({ minLength: 1 }),
18
+ api_key: Type.String({ minLength: 1 }),
19
+ models: Type.Array(Type.Object({
20
+ name: Type.String(),
21
+ context_window: Type.Optional(Type.Number()),
22
+ patches: Type.Optional(Type.Array(Type.String())),
23
+ })),
24
+ concurrency_mode: Type.Optional(Type.Union([Type.Literal("auto"), Type.Literal("manual"), Type.Literal("none")])),
25
+ max_concurrency: Type.Optional(Type.Number()),
26
+ queue_timeout_ms: Type.Optional(Type.Number()),
27
+ max_queue_size: Type.Optional(Type.Number()),
28
+ });
29
+ const QuickSetupMappingSchema = Type.Object({
30
+ client_model: Type.String({ minLength: 1 }),
31
+ backend_model: Type.String({ minLength: 1 }),
32
+ });
33
+ const QuickSetupRetryRuleSchema = Type.Object({
34
+ name: Type.String({ minLength: 1 }),
35
+ status_code: Type.Number({ minimum: 100, maximum: 599 }),
36
+ body_pattern: Type.String({ minLength: 1 }),
37
+ retry_strategy: Type.Union([Type.Literal("fixed"), Type.Literal("exponential")]),
38
+ retry_delay_ms: Type.Number({ minimum: 100 }),
39
+ max_retries: Type.Number({ minimum: 0, maximum: 100 }),
40
+ max_delay_ms: Type.Number({ minimum: 100 }),
41
+ });
42
+ const QuickSetupTransformSchema = Type.Object({
43
+ inject_headers: Type.Optional(Type.Record(Type.String(), Type.String())),
44
+ request_defaults: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
45
+ drop_fields: Type.Optional(Type.Array(Type.String())),
46
+ });
47
+ const QuickSetupSchema = Type.Object({
48
+ provider: QuickSetupProviderSchema,
49
+ mappings: Type.Array(QuickSetupMappingSchema),
50
+ retry_rules: Type.Array(QuickSetupRetryRuleSchema),
51
+ transform_rules: Type.Optional(QuickSetupTransformSchema),
52
+ });
53
+ export const adminQuickSetupRoutes = (app, options, done) => {
54
+ const { db, stateRegistry, tracker, adaptiveController } = options;
55
+ app.post("/admin/api/quick-setup", { schema: { body: QuickSetupSchema } }, async (request, reply) => {
56
+ const body = request.body;
57
+ // 1. Validate provider name
58
+ if (!PROVIDER_NAME_RE.test(body.provider.name)) {
59
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "Provider 名称仅允许英文大小写字母、数字、横线和下划线"));
60
+ }
61
+ // 2. Check no duplicate provider name
62
+ const existing = db.prepare("SELECT id FROM providers WHERE name = ?").get(body.provider.name);
63
+ if (existing) {
64
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, `Provider 名称 '${body.provider.name}' 已存在`));
65
+ }
66
+ // 3. Validate retry rule body_pattern regex
67
+ for (const rule of body.retry_rules) {
68
+ try {
69
+ new RegExp(rule.body_pattern);
70
+ }
71
+ catch {
72
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.INVALID_REGEX, `重试规则「${rule.name}」的 body_pattern 不是有效的正则表达式`));
73
+ }
74
+ }
75
+ // 4. Start transaction
76
+ const encryptionKey = getSetting(db, "encryption_key");
77
+ const createAll = db.transaction(() => {
78
+ // 5. Create provider with models JSON
79
+ const encryptedKey = encrypt(body.provider.api_key, encryptionKey);
80
+ const modelEntries = body.provider.models.map(m => ({
81
+ name: m.name,
82
+ ...(m.context_window != null ? { context_window: m.context_window } : {}),
83
+ ...(m.patches && m.patches.length > 0 ? { patches: m.patches } : {}),
84
+ }));
85
+ const adaptiveEnabled = body.provider.concurrency_mode === 'auto' ? 1 : 0;
86
+ const maxConcurrency = body.provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency;
87
+ const queueTimeoutMs = body.provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms;
88
+ const maxQueueSize = body.provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size;
89
+ const providerId = createProvider(db, {
90
+ name: body.provider.name,
91
+ api_type: body.provider.api_type,
92
+ base_url: body.provider.base_url,
93
+ api_key: encryptedKey,
94
+ api_key_preview: body.provider.api_key.length > API_KEY_PREVIEW_MIN_LENGTH
95
+ ? `${body.provider.api_key.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${body.provider.api_key.slice(-API_KEY_PREVIEW_PREFIX_LEN)}`
96
+ : "****",
97
+ models: JSON.stringify(modelEntries),
98
+ is_active: 1,
99
+ max_concurrency: maxConcurrency,
100
+ queue_timeout_ms: queueTimeoutMs,
101
+ max_queue_size: maxQueueSize,
102
+ adaptive_enabled: adaptiveEnabled,
103
+ });
104
+ // 6. Upsert mapping groups
105
+ for (const m of body.mappings) {
106
+ const existing = db.prepare('SELECT id FROM mapping_groups WHERE client_model = ?').get(m.client_model);
107
+ const ruleJson = JSON.stringify({
108
+ targets: [{ backend_model: m.backend_model, provider_id: providerId }],
109
+ });
110
+ if (existing) {
111
+ updateMappingGroup(db, existing.id, {
112
+ client_model: m.client_model,
113
+ rule: ruleJson,
114
+ });
115
+ }
116
+ else {
117
+ createMappingGroup(db, {
118
+ client_model: m.client_model,
119
+ rule: ruleJson,
120
+ });
121
+ }
122
+ }
123
+ // 7. Create retry rules
124
+ for (const r of body.retry_rules) {
125
+ createRetryRule(db, {
126
+ name: r.name,
127
+ status_code: r.status_code,
128
+ body_pattern: r.body_pattern,
129
+ is_active: 1,
130
+ retry_strategy: r.retry_strategy,
131
+ retry_delay_ms: r.retry_delay_ms,
132
+ max_retries: r.max_retries,
133
+ max_delay_ms: r.max_delay_ms,
134
+ });
135
+ }
136
+ // 8. Create transform rules
137
+ if (body.transform_rules) {
138
+ upsertTransformRule(db, providerId, {
139
+ inject_headers: body.transform_rules.inject_headers ?? null,
140
+ request_defaults: body.transform_rules.request_defaults ?? null,
141
+ drop_fields: body.transform_rules.drop_fields ?? null,
142
+ is_active: 1,
143
+ });
144
+ }
145
+ return providerId;
146
+ });
147
+ // 8. Execute transaction
148
+ const providerId = createAll();
149
+ // 9. Sync concurrency state
150
+ const finalAdaptiveEnabled = body.provider.concurrency_mode === 'auto' ? 1 : 0;
151
+ const finalMaxConcurrency = body.provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency;
152
+ const finalQueueTimeoutMs = body.provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms;
153
+ const finalMaxQueueSize = body.provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size;
154
+ adaptiveController?.syncProvider(providerId, {
155
+ adaptive_enabled: finalAdaptiveEnabled,
156
+ max_concurrency: finalMaxConcurrency,
157
+ queue_timeout_ms: finalQueueTimeoutMs,
158
+ max_queue_size: finalMaxQueueSize,
159
+ });
160
+ tracker?.updateProviderConfig(providerId, {
161
+ name: body.provider.name,
162
+ maxConcurrency: finalMaxConcurrency,
163
+ queueTimeoutMs: finalQueueTimeoutMs,
164
+ maxQueueSize: finalMaxQueueSize,
165
+ });
166
+ return reply.code(HTTP_CREATED).send({ success: true, provider_id: providerId });
167
+ });
168
+ done();
169
+ };
@@ -7,7 +7,11 @@ export const adminRecommendedRoutes = (app, options, done) => {
7
7
  app.get("/admin/api/recommended/retry-rules", async (_req, reply) => {
8
8
  const rules = getRecommendedRetryRules();
9
9
  const existing = new Set(db.prepare("SELECT name FROM retry_rules").all().map((r) => r.name));
10
- return reply.send(rules.filter((r) => !existing.has(r.name)));
10
+ // Return all rules with `exists` flag, so the frontend can show all and mark existing ones
11
+ return reply.send(rules.map(r => ({
12
+ ...r,
13
+ exists: existing.has(r.name),
14
+ })));
11
15
  });
12
16
  app.post("/admin/api/recommended/reload", async (_req, reply) => {
13
17
  reloadConfig();
@@ -14,6 +14,7 @@ import { adminSettingsRoutes } from "./settings.js";
14
14
  import { adminRecommendedRoutes } from "./recommended.js";
15
15
  import { adminUsageRoutes } from "./usage.js";
16
16
  import { adminUpgradeRoutes } from "./upgrade.js";
17
+ import { adminQuickSetupRoutes } from "./quick-setup.js";
17
18
  import { adminImportExportRoutes } from "./settings-import-export.js";
18
19
  import { adminTransformRuleRoutes } from "./transform-rules.js";
19
20
  import { adminScheduleRoutes } from "./schedules.js";
@@ -37,6 +38,7 @@ export const adminRoutes = (app, options, done) => {
37
38
  app.register(adminImportExportRoutes, { db: options.db, stateRegistry: options.stateRegistry, pluginRegistry: options.pluginRegistry });
38
39
  app.register(adminRecommendedRoutes, { db: options.db });
39
40
  app.register(adminUsageRoutes, { db: options.db });
41
+ app.register(adminQuickSetupRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController });
40
42
  app.register(adminUpgradeRoutes, { db: options.db, closeFn: options.closeFn ?? (async () => { }) });
41
43
  app.register(adminTransformRuleRoutes, { db: options.db, pluginRegistry: options.pluginRegistry });
42
44
  done();
@@ -1,10 +1,16 @@
1
1
  export interface ModelInfo {
2
2
  name: string;
3
3
  context_window: number | null;
4
+ patches: string[];
5
+ }
6
+ export interface ModelEntry {
7
+ name: string;
8
+ context_window?: number;
9
+ patches?: string[];
4
10
  }
5
11
  export declare const MODEL_CONTEXT_WINDOWS: Record<string, number>;
6
12
  export declare const DEFAULT_CONTEXT_WINDOW = 200000;
7
13
  export declare const OVERFLOW_THRESHOLD = 1000000;
8
14
  export declare function lookupContextWindow(modelName: string): number;
9
- export declare function parseModels(raw: string): string[];
10
- export declare function buildModelInfoList(modelNames: string[], overrides: Map<string, number>): ModelInfo[];
15
+ export declare function parseModels(raw: string): ModelEntry[];
16
+ export declare function buildModelInfoList(modelEntries: ModelEntry[], overrides: Map<string, number>): ModelInfo[];
@@ -91,15 +91,27 @@ export function parseModels(raw) {
91
91
  const parsed = JSON.parse(raw);
92
92
  if (!Array.isArray(parsed))
93
93
  return [];
94
- return parsed.map((item) => typeof item === 'string' ? item : item?.name ?? '').filter(Boolean);
94
+ return parsed.map((item) => {
95
+ if (typeof item === 'string') {
96
+ return item ? { name: item, patches: [] } : null;
97
+ }
98
+ const obj = item;
99
+ if (!obj || !obj.name)
100
+ return null;
101
+ return {
102
+ name: obj.name,
103
+ patches: obj.patches ?? [],
104
+ };
105
+ }).filter((e) => e !== null);
95
106
  }
96
107
  catch {
97
108
  return [];
98
109
  }
99
110
  }
100
- export function buildModelInfoList(modelNames, overrides) {
101
- return modelNames.map(name => ({
102
- name,
103
- context_window: overrides.get(name) ?? lookupContextWindow(name),
111
+ export function buildModelInfoList(modelEntries, overrides) {
112
+ return modelEntries.map(entry => ({
113
+ name: entry.name,
114
+ context_window: overrides.get(entry.name) ?? lookupContextWindow(entry.name),
115
+ patches: entry.patches ?? [],
104
116
  }));
105
117
  }
@@ -17,6 +17,7 @@ export interface RecommendedRetryRule {
17
17
  retry_delay_ms: number;
18
18
  max_retries: number;
19
19
  max_delay_ms: number;
20
+ providers?: string[];
20
21
  }
21
22
  export declare function loadRecommendedConfig(dir?: string): void;
22
23
  export declare function getRecommendedProviders(): ProviderGroup[];
@@ -1,22 +1,18 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- let cachedProviders = [];
4
- let cachedRetryRules = [];
5
3
  let configDir = '';
6
4
  export function loadRecommendedConfig(dir) {
7
5
  configDir = dir ?? path.resolve(process.cwd(), 'config');
8
- cachedProviders = loadJson('recommended-providers.json');
9
- cachedRetryRules = loadJson('recommended-retry-rules.json');
10
6
  }
11
7
  export function getRecommendedProviders() {
12
- return cachedProviders;
8
+ return loadJson('recommended-providers.json');
13
9
  }
14
10
  export function getRecommendedRetryRules() {
15
- return cachedRetryRules;
16
- }
17
- export function reloadConfig() {
18
- loadRecommendedConfig(configDir);
11
+ return loadJson('recommended-retry-rules.json');
19
12
  }
13
+ // No-op: kept for backward compat (reload endpoint, upgrade flow)
14
+ // Config is now always read from disk, no caching.
15
+ export function reloadConfig() { }
20
16
  function loadJson(filename) {
21
17
  const filePath = path.join(configDir, filename);
22
18
  try {
@@ -13,6 +13,7 @@ import { insertRejectedLog } from "../log-helpers.js";
13
13
  import { ToolLoopGuard } from "../loop-prevention/tool-loop-guard.js";
14
14
  import { buildTransportFn } from "../transport/transport-fn.js";
15
15
  import { applyOverflowRedirect } from "../routing/overflow.js";
16
+ import { parseModels } from "../../config/model-context.js";
16
17
  import { applyProviderPatches } from "../patch/index.js";
17
18
  import { PipelineSnapshot } from "../pipeline-snapshot.js";
18
19
  import { maybeInjectModelInfoTag } from "../response-transform.js";
@@ -223,8 +224,13 @@ async function executeFailoverLoop(ctx) {
223
224
  pluginRegistry.applyAfterRequest(pluginCtx);
224
225
  injectedHeaders = pluginCtx.headers;
225
226
  }
226
- // provider patches — 使用返回值
227
- const { body: patchedBody, meta: patchMeta } = applyProviderPatches(currentBody, provider);
227
+ // provider patches — 优先从 DB models JSON 读取 patch 配置,无配置时回退自动检测
228
+ const providerModels = parseModels(provider.models || "[]");
229
+ const { body: patchedBody, meta: patchMeta } = applyProviderPatches(currentBody, {
230
+ base_url: provider.base_url,
231
+ api_type: provider.api_type,
232
+ models: providerModels,
233
+ });
228
234
  iterationSnapshot.add({ stage: "provider_patch", types: patchMeta.types });
229
235
  const encryptionKey = getSetting(deps.db, "encryption_key");
230
236
  if (!encryptionKey) {
@@ -1,12 +1,15 @@
1
+ import type { ModelEntry } from "../../config/model-context.js";
1
2
  export interface ProviderInfo {
2
3
  base_url: string;
3
4
  api_type: string;
5
+ models?: ModelEntry[];
4
6
  }
5
7
  export interface ProviderPatchMeta {
6
8
  types: string[];
7
9
  }
8
10
  /**
9
11
  * 根据 provider 信息分发到对应的补丁逻辑。
12
+ * 优先使用 DB 配置的 patches 模式,无配置时回退到自动检测。
10
13
  * 返回浅拷贝 body + 执行的补丁类型列表,不修改原始 body。
11
14
  */
12
15
  export declare function applyProviderPatches(body: Record<string, unknown>, provider: ProviderInfo): {
@@ -2,6 +2,7 @@ import { applyDeepSeekPatches } from "./deepseek/index.js";
2
2
  const OPENAI_ORIGIN_HOSTS = ["api.openai.com", "openai.com"];
3
3
  /**
4
4
  * 根据 provider 信息分发到对应的补丁逻辑。
5
+ * 优先使用 DB 配置的 patches 模式,无配置时回退到自动检测。
5
6
  * 返回浅拷贝 body + 执行的补丁类型列表,不修改原始 body。
6
7
  */
7
8
  export function applyProviderPatches(body, provider) {
@@ -15,6 +16,33 @@ export function applyProviderPatches(body, provider) {
15
16
  }
16
17
  return patched;
17
18
  };
19
+ // ---- DB-driven mode:通过 provider.models 配置的 patches 驱动 ----
20
+ if (provider.models) {
21
+ const modelName = body.model ?? "";
22
+ const modelEntry = provider.models.find(m => m.name === modelName);
23
+ const modelPatches = modelEntry?.patches ?? [];
24
+ if (modelPatches.length > 0) {
25
+ // developer_role 补丁(仅 openai 格式需要)
26
+ if (modelPatches.includes("developer_role") && provider.api_type === "openai" && hasDeveloperRole(body)) {
27
+ patchDeveloperRole(ensureCloned());
28
+ patches.push("developer_role");
29
+ }
30
+ // DeepSeek Anthropic 补丁
31
+ const dsAnthropicPatches = ["thinking-param", "cache-control", "thinking-blocks", "orphan-tool-results"];
32
+ if (dsAnthropicPatches.some(p => modelPatches.includes(p)) && provider.api_type === "anthropic") {
33
+ applyDeepSeekPatches(ensureCloned(), "anthropic");
34
+ patches.push("deepseek");
35
+ }
36
+ // DeepSeek OpenAI 补丁
37
+ const dsOpenAIPatches = ["non-ds-tools", "orphan-tool-results-oa"];
38
+ if (dsOpenAIPatches.some(p => modelPatches.includes(p)) && provider.api_type === "openai") {
39
+ applyDeepSeekPatches(ensureCloned(), "openai");
40
+ patches.push("deepseek");
41
+ }
42
+ return { body: patched ?? body, meta: { types: patches } };
43
+ }
44
+ }
45
+ // ---- 回退模式:自动检测(保持现有逻辑不变)----
18
46
  // 通用补丁:OpenAI 兼容 provider(非 OpenAI 原生)不支持 developer role
19
47
  if (provider.api_type === "openai" && !isOpenAIOrigin(provider.base_url)) {
20
48
  if (hasDeveloperRole(body)) {
@@ -0,0 +1 @@
1
+ import{Ft as e,K as t,Lt as n,Y as r,r as i,st as a,ut as o}from"./button-zud8Qspb.js";var s=[`data-size`],c=r({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(r){let c=r;return(l,u)=>(a(),t(`div`,{"data-slot":`card`,"data-size":r.size,class:n(e(i)(`ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-lg py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col`,c.class))},[o(l.$slots,`default`)],10,s))}}),l=r({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(a(),t(`div`,{"data-slot":`card-content`,class:n(e(i)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[o(r.$slots,`default`)],2))}});export{c as n,l as t};
@@ -0,0 +1 @@
1
+ import{Ft as e,K as t,Lt as n,Y as r,r as i,st as a,ut as o}from"./button-zud8Qspb.js";var s=r({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(a(),t(`div`,{"data-slot":`card-header`,class:n(e(i)(`gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]`,s.class))},[o(r.$slots,`default`)],2))}}),c=r({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(a(),t(`div`,{"data-slot":`card-title`,class:n(e(i)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[o(r.$slots,`default`)],2))}});export{s as n,c as t};
@@ -0,0 +1 @@
1
+ import{B as e,Bt as t,Ft as n,G as r,H as i,J as a,K as o,Lt as s,M as c,Ot as l,U as u,W as d,Y as f,bt as p,lt as m,st as h}from"./button-zud8Qspb.js";import{a as g}from"./PopperContent-Bd_mpt_D.js";import{n as _,r as v,t as y}from"./PopoverTrigger-DSmA2dE4.js";var b=c(`chevron-right`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),x=c(`plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),S=[`onMouseenter`],C={class:`truncate max-w-40`},w={key:0,class:`ml-1 text-[10px] px-1 py-px rounded bg-emerald-500/15 text-emerald-400 shrink-0`},T=[`onMouseenter`],E=[`onClick`],D={class:`truncate`},O={key:0,class:`shrink-0 text-xs text-muted-foreground`},k={key:0,class:`px-2 py-1.5 text-sm text-muted-foreground`},A=f({__name:`CascadingSelect`,props:{groups:{},modelValue:{},placeholder:{default:`请选择...`},compact:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(c,{emit:f}){let x=c,A=f,j=l(!1),M=l(null),N=i(()=>{if(!x.modelValue)return``;let e=x.groups.find(e=>e.key===x.modelValue.groupKey);if(!e)return``;let t=e.options.find(e=>e.value===x.modelValue.value);return t?`${e.label} / ${t.label}`:``});function P(e,t){A(`update:modelValue`,{groupKey:e,value:t}),j.value=!1}function F(e){j.value=e,e||(M.value=null)}return(i,l)=>(h(),d(n(v),{open:j.value,"onUpdate:open":F},{default:p(()=>[a(n(y),{"as-child":``},{default:p(()=>[u(`div`,{class:s([`flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer hover:bg-accent hover:text-accent-foreground`,[c.compact?`h-8 text-xs px-2.5 py-1`:`h-10 text-sm px-3 py-2`,{"ring-2 ring-ring ring-offset-2":j.value}]])},[u(`span`,{class:s([`truncate`,c.modelValue?`text-foreground`:`text-muted-foreground`])},t(N.value||c.placeholder),3),a(n(g),{class:`h-4 w-4 shrink-0 opacity-50`})],2)]),_:1}),a(n(_),{align:`start`,"side-offset":4,class:`z-[200] w-auto min-w-56 overflow-visible p-1`},{default:p(()=>[(h(!0),o(e,null,m(c.groups,i=>(h(),o(`div`,{key:i.key,class:s([`relative flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground`,{"bg-accent text-accent-foreground z-10":M.value===i.key}]),onMouseenter:e=>M.value=i.key},[u(`span`,C,t(i.label),1),i.badge?(h(),o(`span`,w,t(i.badge),1)):r(``,!0),a(n(b),{class:`ml-1 h-4 w-4 shrink-0 opacity-50`}),M.value===i.key&&i.options.length>0?(h(),o(`div`,{key:1,class:`absolute left-full top-0 ml-0.5 min-w-48 rounded-md border bg-popover p-1 text-popover-foreground shadow-md`,onMouseenter:e=>M.value=i.key},[(h(!0),o(e,null,m(i.options,e=>(h(),o(`div`,{key:e.value,class:s([`flex cursor-pointer items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground`,{"bg-accent text-accent-foreground":c.modelValue?.groupKey===i.key&&c.modelValue?.value===e.value}]),onClick:t=>P(i.key,e.value)},[u(`span`,D,t(e.label),1),e.tag?(h(),o(`span`,O,t(e.tag),1)):r(``,!0)],10,E))),128))],40,T)):r(``,!0)],42,S))),128)),c.groups.length===0?(h(),o(`div`,k,` 暂无选项 `)):r(``,!0)]),_:1})]),_:1},8,[`open`]))}}),j=f({__name:`CascadingModelSelect`,props:{providers:{},modelValue:{},placeholder:{default:`选择供应商 / 模型`},compact:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:t}){let n=e,r=t;function a(e){return e>=1e6?`${e/1e6}M`:`${e/1e3}K`}let o=i(()=>n.providers.map(e=>({key:e.provider.id,label:e.provider.name,badge:e.isNew?`新`:void 0,options:e.models.map(e=>({value:e.name,label:e.name,tag:a(e.contextWindow)}))}))),s=i(()=>n.modelValue?{groupKey:n.modelValue.provider_id,value:n.modelValue.model}:void 0);function c(e){r(`update:modelValue`,{provider_id:e.groupKey,model:e.value})}return(t,n)=>(h(),d(A,{groups:o.value,"model-value":s.value,placeholder:e.placeholder,compact:e.compact,"onUpdate:modelValue":c},null,8,[`groups`,`model-value`,`placeholder`,`compact`]))}});export{x as n,j as t};
@@ -0,0 +1 @@
1
+ import{Ft as e,G as t,H as n,J as r,R as i,Rt as a,W as o,Y as s,Z as c,bt as l,ft as u,i as d,m as f,nt as p,o as m,r as h,st as g,ut as _,x as v,z as y}from"./button-zud8Qspb.js";import{t as b}from"./check-CRv7NpkT.js";import{t as x}from"./ohash.D__AXeF1-D5e5Wyzx.js";import{g as S,o as C,u as w,y as T}from"./Teleport-CLw1Jxrb.js";import{t as E}from"./useFormControl-OyxyVR_M.js";import{t as D}from"./VisuallyHiddenInput-CCL5ykZW.js";import{t as O}from"./RovingFocusItem-DwGTruuB.js";function k(e,t){return S(e)?!1:Array.isArray(e)?e.some(e=>x(e,t)):x(e,t)}var[A,j]=T(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=T(`CheckboxRoot`),I=s({inheritAttrs:!1,__name:`CheckboxRoot`,props:{defaultValue:{type:null,required:!1},modelValue:{type:null,required:!1,default:void 0},disabled:{type:Boolean,required:!1},value:{type:null,required:!1,default:`on`},id:{type:String,required:!1},trueValue:{type:null,required:!1,default:()=>!0},falseValue:{type:null,required:!1,default:()=>!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`},name:{type:String,required:!1},required:{type:Boolean,required:!1}},emits:[`update:modelValue`],setup(r,{emit:a}){let s=r,c=a,{forwardRef:h,currentElement:v}=m(),b=A(null),C=f(s,`modelValue`,c,{defaultValue:s.defaultValue??s.falseValue,passive:s.modelValue===void 0}),w=n(()=>b?.disabled.value||s.disabled),T=n(()=>x(C.value,s.trueValue)),j=n(()=>S(b?.modelValue.value)?C.value===`indeterminate`?`indeterminate`:T.value:k(b.modelValue.value,s.value));function P(){if(S(b?.modelValue.value))C.value===`indeterminate`?C.value=s.trueValue:C.value=T.value?s.falseValue:s.trueValue;else{let e=[...b.modelValue.value||[]];if(k(e,s.value)){let t=e.findIndex(e=>x(e,s.value));e.splice(t,1)}else e.push(s.value);b.modelValue.value=e}}let I=E(v),L=n(()=>s.id&&v.value?document.querySelector(`[for="${s.id}"]`)?.innerText:void 0);return F({disabled:w,state:j}),(n,r)=>(g(),o(u(e(b)?.rovingFocus.value?e(O):e(d)),p(n.$attrs,{id:n.id,ref:e(h),role:`checkbox`,"as-child":n.asChild,as:n.as,type:n.as===`button`?`button`:void 0,"aria-checked":e(M)(j.value)?`mixed`:j.value,"aria-required":n.required,"aria-label":n.$attrs[`aria-label`]||L.value,"data-state":e(N)(j.value),"data-disabled":w.value?``:void 0,disabled:w.value,focusable:e(b)?.rovingFocus.value?!w.value:void 0,onKeydown:i(y(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:l(()=>[_(n.$slots,`default`,{modelValue:e(C),state:j.value}),e(I)&&n.name&&!e(b)?(g(),o(e(D),{key:0,type:`checkbox`,checked:!!j.value,name:n.name,value:n.value,disabled:w.value,required:n.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):t(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=s({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(t){let{forwardRef:n}=m(),i=P();return(t,a)=>(g(),o(e(C),{present:t.forceMount||e(M)(e(i).state.value)||e(i).state.value===!0},{default:l(()=>[r(e(d),p({ref:e(n),"data-state":e(N)(e(i).state.value),"data-disabled":e(i).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":t.asChild,as:t.as},t.$attrs),{default:l(()=>[_(t.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=s({__name:`Checkbox`,props:{defaultValue:{},modelValue:{},disabled:{type:Boolean},value:{},id:{},trueValue:{},falseValue:{},asChild:{type:Boolean},as:{},name:{},required:{type:Boolean},class:{type:[Boolean,null,String,Object,Array]}},emits:[`update:modelValue`],setup(t,{emit:n}){let i=t,s=n,u=w(v(i,`class`),s);return(t,n)=>(g(),o(e(I),p({"data-slot":`checkbox`},e(u),{class:e(h)(`border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-md border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50`,i.class)}),{default:l(n=>[r(e(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:l(()=>[_(t.$slots,`default`,a(c(n)),()=>[r(e(b))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
@@ -0,0 +1 @@
1
+ import{Ft as e,G as t,H as n,J as r,Nt as i,Ot as a,Rt as o,W as s,Y as c,Z as l,_t as u,at as d,bt as f,d as p,i as m,m as h,nt as g,o as _,rt as v,st as y,ut as b}from"./button-zud8Qspb.js";import{c as x,o as S,u as C,y as w}from"./Teleport-CLw1Jxrb.js";var[T,E]=w(`CollapsibleRoot`),D=c({__name:`CollapsibleRoot`,props:{defaultOpen:{type:Boolean,required:!1,default:!1},open:{type:Boolean,required:!1,default:void 0},disabled:{type:Boolean,required:!1},unmountOnHide:{type:Boolean,required:!1,default:!0},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`update:open`],setup(t,{expose:n,emit:r}){let a=t,o=h(a,`open`,r,{defaultValue:a.defaultOpen,passive:a.open===void 0}),{disabled:c,unmountOnHide:l}=i(a);return E({contentId:``,disabled:c,open:o,unmountOnHide:l,onOpenToggle:()=>{c.value||(o.value=!o.value)}}),n({open:o}),_(),(t,n)=>(y(),s(e(m),{as:t.as,"as-child":a.asChild,"data-state":e(o)?`open`:`closed`,"data-disabled":e(c)?``:void 0},{default:f(()=>[b(t.$slots,`default`,{open:e(o)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=c({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(i,{emit:o}){let c=i,l=o,h=T();h.contentId||=x(void 0,`reka-collapsible-content`);let C=a(),{forwardRef:w,currentElement:E}=_(),D=a(0),O=a(0),k=n(()=>h.open.value),A=a(k.value),j=a();u(()=>[k.value,C.value?.present],async()=>{await v();let e=E.value;if(!e)return;j.value=j.value||{transitionDuration:e.style.transitionDuration,animationName:e.style.animationName},e.style.transitionDuration=`0s`,e.style.animationName=`none`;let t=e.getBoundingClientRect();O.value=t.height,D.value=t.width,A.value||(e.style.transitionDuration=j.value.transitionDuration,e.style.animationName=j.value.animationName)},{immediate:!0});let M=n(()=>A.value&&h.open.value);return d(()=>{requestAnimationFrame(()=>{A.value=!1})}),p(E,`beforematch`,e=>{requestAnimationFrame(()=>{h.onOpenToggle(),l(`contentFound`)})}),(n,i)=>(y(),s(e(S),{ref_key:`presentRef`,ref:C,present:n.forceMount||e(h).open.value,"force-mount":!0},{default:f(({present:i})=>[r(e(m),g(n.$attrs,{id:e(h).contentId,ref:e(w),"as-child":c.asChild,as:n.as,hidden:i?void 0:e(h).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:e(h).open.value?`open`:`closed`,"data-disabled":e(h).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:f(()=>[!e(h).unmountOnHide.value||i?b(n.$slots,`default`,{key:0}):t(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=c({__name:`Collapsible`,props:{defaultOpen:{type:Boolean},open:{type:Boolean},disabled:{type:Boolean},unmountOnHide:{type:Boolean},asChild:{type:Boolean},as:{}},emits:[`update:open`],setup(t,{emit:n}){let r=C(t,n);return(t,n)=>(y(),s(e(D),g({"data-slot":`collapsible`},e(r)),{default:f(e=>[b(t.$slots,`default`,o(l(e)))]),_:3},16))}}),A=c({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(t){let n=t;return(t,r)=>(y(),s(e(O),g({"data-slot":`collapsible-content`},n),{default:f(()=>[b(t.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};
@@ -0,0 +1 @@
1
+ import{Ft as e,W as t,Y as n,bt as r,i,nt as a,o,st as s,ut as c}from"./button-zud8Qspb.js";import{r as l}from"./CollapsibleContent-CdeCo0Ko.js";var u=n({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(n){let a=n;o();let u=l();return(n,o)=>(s(),t(e(i),{type:n.as===`button`?`button`:void 0,as:n.as,"as-child":a.asChild,"aria-controls":e(u).contentId,"aria-expanded":e(u).open.value,"data-state":e(u).open.value?`open`:`closed`,"data-disabled":e(u).disabled?.value?``:void 0,disabled:e(u).disabled?.value,onClick:e(u).onOpenToggle},{default:r(()=>[c(n.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),d=n({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(n){let i=n;return(n,o)=>(s(),t(e(u),a({"data-slot":`collapsible-trigger`},i),{default:r(()=>[c(n.$slots,`default`)]),_:3},16))}});export{d as t};
@@ -0,0 +1 @@
1
+ import{$ as e,H as t,Ot as n,Q as r,Tt as i,Y as a,_t as o,a as s,ct as c,vt as l}from"./button-zud8Qspb.js";import{h as u}from"./Teleport-CLw1Jxrb.js";import{n as d}from"./VisuallyHidden-BpDuyh8-.js";function f(e){let r=u({dir:n(`ltr`)});return t(()=>e?.value||r.dir?.value||`ltr`)}var p=`data-reka-collection-item`;function m(u={}){let{key:f=``,isProvider:m=!1}=u,h=`${f}CollectionProvider`,g;if(m){let e=n(new Map);g={collectionRef:n(),itemMap:e},c(h,g)}else g=e(h);let _=(e=!1)=>{let t=g.collectionRef.value;if(!t)return[];let n=Array.from(t.querySelectorAll(`[${p}]`)),r=Array.from(g.itemMap.value.values()).sort((e,t)=>n.indexOf(e.ref)-n.indexOf(t.ref));return e?r:r.filter(e=>e.ref.dataset.disabled!==``)},v=a({name:`CollectionSlot`,inheritAttrs:!1,setup(e,{slots:t,attrs:n}){let{primitiveElement:i,currentElement:a}=d();return o(a,()=>{g.collectionRef.value=a.value}),()=>r(s,{ref:i,...n},t)}}),y=a({name:`CollectionItem`,inheritAttrs:!1,props:{value:{validator:()=>!0}},setup(e,{slots:t,attrs:n}){let{primitiveElement:a,currentElement:o}=d();return l(t=>{if(o.value){let n=i(o.value);g.itemMap.value.set(n,{ref:o.value,value:e.value}),t(()=>g.itemMap.value.delete(n))}}),()=>r(s,{...n,[p]:``,ref:a},t)}});return{getItems:_,reactiveItems:t(()=>Array.from(g.itemMap.value.values())),itemMapSize:t(()=>g.itemMap.value.size),CollectionSlot:v,CollectionItem:y}}export{f as n,m as t};