llm-simple-router 0.6.5 → 0.6.6

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 (81) hide show
  1. package/dist/admin/providers.d.ts +2 -0
  2. package/dist/admin/providers.js +30 -1
  3. package/dist/admin/routes.d.ts +2 -0
  4. package/dist/admin/routes.js +1 -1
  5. package/dist/db/migrations/033_add_adaptive_concurrency.sql +3 -0
  6. package/dist/db/providers.d.ts +3 -1
  7. package/dist/db/providers.js +3 -3
  8. package/dist/index.js +13 -2
  9. package/dist/monitor/request-tracker.d.ts +3 -0
  10. package/dist/monitor/request-tracker.js +7 -0
  11. package/dist/monitor/types.d.ts +2 -0
  12. package/dist/proxy/adaptive-controller.d.ts +42 -0
  13. package/dist/proxy/adaptive-controller.js +130 -0
  14. package/dist/proxy/anthropic.d.ts +2 -0
  15. package/dist/proxy/anthropic.js +2 -2
  16. package/dist/proxy/openai.d.ts +2 -0
  17. package/dist/proxy/openai.js +2 -2
  18. package/dist/proxy/orchestrator.d.ts +3 -1
  19. package/dist/proxy/orchestrator.js +32 -18
  20. package/frontend-dist/assets/{CardContent-DG1NiXMU.js → CardContent-jQcfCC7J.js} +1 -1
  21. package/frontend-dist/assets/{CardTitle-6JvqGNd9.js → CardTitle-BrCTvULL.js} +1 -1
  22. package/frontend-dist/assets/{CascadingModelSelect-BzDHmRLv.js → CascadingModelSelect-BFh67j5d.js} +1 -1
  23. package/frontend-dist/assets/{Checkbox-D4TrUHCb.js → Checkbox-Bbt7JpdE.js} +1 -1
  24. package/frontend-dist/assets/{CollapsibleTrigger-ZJCGAkxi.js → CollapsibleTrigger-DMnEA0qC.js} +1 -1
  25. package/frontend-dist/assets/{Collection-CRTZGViV.js → Collection-CVk3TPHc.js} +1 -1
  26. package/frontend-dist/assets/{Dashboard-C-B4p8HM.js → Dashboard-Coftbg4B.js} +1 -1
  27. package/frontend-dist/assets/{DialogTitle-CecSHUUq.js → DialogTitle-BbOAZzPQ.js} +1 -1
  28. package/frontend-dist/assets/{Input-DoCE9j9O.js → Input-DdHY9q0w.js} +1 -1
  29. package/frontend-dist/assets/{Label-6sa_UFaw.js → Label-DRQv_Dr_.js} +1 -1
  30. package/frontend-dist/assets/{Login-BkCJQFjz.js → Login-SV3ctFnJ.js} +1 -1
  31. package/frontend-dist/assets/{Logs-HG_BuJe5.js → Logs-BG45kX6E.js} +1 -1
  32. package/frontend-dist/assets/{ModelMappings-CjI27TDc.js → ModelMappings-DEaBnRU3.js} +1 -1
  33. package/frontend-dist/assets/Monitor-ZHOt11n-.js +1 -0
  34. package/frontend-dist/assets/{PopoverTrigger-D0nT5UVQ.js → PopoverTrigger-z-Z3EjBk.js} +1 -1
  35. package/frontend-dist/assets/{PopperContent-BbE-uPX0.js → PopperContent-DPC-6a3n.js} +1 -1
  36. package/frontend-dist/assets/Providers-DpY6pAcg.js +1 -0
  37. package/frontend-dist/assets/{ProxyEnhancement-DQixi_0_.js → ProxyEnhancement-D6KBDXMp.js} +1 -1
  38. package/frontend-dist/assets/{RetryRules-B5r18TFL.js → RetryRules-DWI7_WLZ.js} +1 -1
  39. package/frontend-dist/assets/{RouterKeys-BwdoieS_.js → RouterKeys-CZ1657eX.js} +1 -1
  40. package/frontend-dist/assets/{RovingFocusItem-CpDEc1ox.js → RovingFocusItem-BREE2YEV.js} +1 -1
  41. package/frontend-dist/assets/{Schedules-C0q4rt97.js → Schedules-BVPsBRPi.js} +1 -1
  42. package/frontend-dist/assets/{SelectValue-CKJWYmgi.js → SelectValue-H8hwQwbk.js} +1 -1
  43. package/frontend-dist/assets/{Settings-DQN3_4Gx.js → Settings-DHYaYRgU.js} +1 -1
  44. package/frontend-dist/assets/{Setup-CEk8SRJu.js → Setup-yOYNKkOG.js} +1 -1
  45. package/frontend-dist/assets/{Switch-ThwlPMEz.js → Switch-CojD3rTH.js} +1 -1
  46. package/frontend-dist/assets/{TableHeader-UwRhaVOA.js → TableHeader-awoHTsWN.js} +1 -1
  47. package/frontend-dist/assets/{TabsTrigger-CNV5JhP6.js → TabsTrigger-DTKSFj85.js} +1 -1
  48. package/frontend-dist/assets/{Teleport-BHTgdtZR.js → Teleport-DehYAXud.js} +1 -1
  49. package/frontend-dist/assets/{TooltipTrigger-DKa4gPvX.js → TooltipTrigger-C2dl_dml.js} +1 -1
  50. package/frontend-dist/assets/{UnifiedRequestDialog-CBbZxE1N.js → UnifiedRequestDialog-C8A-uSTR.js} +1 -1
  51. package/frontend-dist/assets/{VisuallyHidden-r2E3heZY.js → VisuallyHidden-C8oaGi2S.js} +1 -1
  52. package/frontend-dist/assets/{VisuallyHiddenInput-DSyTANlz.js → VisuallyHiddenInput-BMc813t2.js} +1 -1
  53. package/frontend-dist/assets/{alert-dialog-CNAhVHUE.js → alert-dialog-C8TZQmU6.js} +1 -1
  54. package/frontend-dist/assets/arrow-down-D-cQXxau.js +1 -0
  55. package/frontend-dist/assets/{badge-RFq5LS37.js → badge-BVh2WpA5.js} +1 -1
  56. package/frontend-dist/assets/{button-D4qBQ0nA.js → button-N59D1BGa.js} +2 -2
  57. package/frontend-dist/assets/check-dDgrw3T3.js +1 -0
  58. package/frontend-dist/assets/{copy-BxjNh73N.js → copy-DTOecxa9.js} +1 -1
  59. package/frontend-dist/assets/{dialog-CLepZTFY.js → dialog-kA7AUNoc.js} +1 -1
  60. package/frontend-dist/assets/{file-text-DqxNey63.js → file-text-DzZCFO7y.js} +1 -1
  61. package/frontend-dist/assets/{index-C4H_2b0G.js → index-B5upNblU.js} +1 -1
  62. package/frontend-dist/assets/index-xjdbFKXJ.css +1 -0
  63. package/frontend-dist/assets/{lib-SdzBxIwM.js → lib-ClDokUbt.js} +1 -1
  64. package/frontend-dist/assets/loader-circle-DVHRL-38.js +1 -0
  65. package/frontend-dist/assets/{useClipboard-DBClUufY.js → useClipboard-DU1ne-Jw.js} +1 -1
  66. package/frontend-dist/assets/{useFocusGuards-B99Wx8XA.js → useFocusGuards-Btmdbg_F.js} +1 -1
  67. package/frontend-dist/assets/useFormControl-C5Kjziuj.js +1 -0
  68. package/frontend-dist/assets/{useLogRetention-CQ7Q54Tt.js → useLogRetention--EGNWXig.js} +1 -1
  69. package/frontend-dist/assets/useNonce-Cp31yRzV.js +1 -0
  70. package/frontend-dist/assets/x-DMktsI_w.js +1 -0
  71. package/frontend-dist/index.html +20 -20
  72. package/package.json +1 -1
  73. package/frontend-dist/assets/Monitor-CElP7aKi.js +0 -1
  74. package/frontend-dist/assets/Providers-DTqfl249.js +0 -1
  75. package/frontend-dist/assets/arrow-down-CFVTVH7t.js +0 -1
  76. package/frontend-dist/assets/check-CkLQpfOO.js +0 -1
  77. package/frontend-dist/assets/index-_Icfkt3I.css +0 -1
  78. package/frontend-dist/assets/loader-circle-Cffc9Uf0.js +0 -1
  79. package/frontend-dist/assets/useFormControl-DuWttDd8.js +0 -1
  80. package/frontend-dist/assets/useNonce-D1Mva4rM.js +0 -1
  81. package/frontend-dist/assets/x-BJjJvWU8.js +0 -1
@@ -1,11 +1,13 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
3
  import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
4
+ import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
4
5
  import type { RequestTracker } from "../monitor/request-tracker.js";
5
6
  interface ProviderRoutesOptions {
6
7
  db: Database.Database;
7
8
  semaphoreManager?: ProviderSemaphoreManager;
8
9
  tracker?: RequestTracker;
10
+ adaptiveController?: AdaptiveConcurrencyController;
9
11
  }
10
12
  export declare const adminProviderRoutes: FastifyPluginCallback<ProviderRoutesOptions>;
11
13
  export {};
@@ -77,6 +77,7 @@ const CreateProviderSchema = Type.Object({
77
77
  max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
78
78
  queue_timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
79
79
  max_queue_size: Type.Optional(Type.Integer({ minimum: 1 })),
80
+ adaptive_enabled: Type.Optional(Type.Integer({ minimum: 0, maximum: 1 })),
80
81
  });
81
82
  const UpdateProviderSchema = Type.Object({
82
83
  name: Type.Optional(Type.String({ minLength: 1 })),
@@ -91,9 +92,10 @@ const UpdateProviderSchema = Type.Object({
91
92
  max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
92
93
  queue_timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
93
94
  max_queue_size: Type.Optional(Type.Integer({ minimum: 1 })),
95
+ adaptive_enabled: Type.Optional(Type.Integer({ minimum: 0, maximum: 1 })),
94
96
  });
95
97
  export const adminProviderRoutes = (app, options, done) => {
96
- const { db, semaphoreManager, tracker } = options;
98
+ const { db, semaphoreManager, tracker, adaptiveController } = options;
97
99
  app.get("/admin/api/providers", async (_request, reply) => {
98
100
  const encryptionKey = getSetting(db, "encryption_key");
99
101
  const providers = getAllProviders(db);
@@ -111,6 +113,7 @@ export const adminProviderRoutes = (app, options, done) => {
111
113
  max_concurrency: s.max_concurrency,
112
114
  queue_timeout_ms: s.queue_timeout_ms,
113
115
  max_queue_size: s.max_queue_size,
116
+ adaptive_enabled: s.adaptive_enabled,
114
117
  concurrency_status: semaphoreManager?.getStatus(s.id) ?? { active: 0, queued: 0 },
115
118
  created_at: s.created_at,
116
119
  updated_at: s.updated_at,
@@ -128,6 +131,7 @@ export const adminProviderRoutes = (app, options, done) => {
128
131
  }
129
132
  const encryptedKey = encrypt(body.api_key, getSetting(db, "encryption_key"));
130
133
  const { names: normalizedModels, overrides: contextOverrides } = extractModelOverrides((body.models ?? []));
134
+ const isAdaptiveEnabled = body.adaptive_enabled ?? 0;
131
135
  const id = createProvider(db, {
132
136
  name: body.name,
133
137
  api_type: body.api_type,
@@ -139,6 +143,7 @@ export const adminProviderRoutes = (app, options, done) => {
139
143
  max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
140
144
  queue_timeout_ms: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
141
145
  max_queue_size: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
146
+ adaptive_enabled: isAdaptiveEnabled,
142
147
  });
143
148
  if (contextOverrides.length > 0) {
144
149
  setModelInfoForProvider(db, id, contextOverrides.map(o => ({ model_name: o.name, context_window: o.context_window })));
@@ -148,6 +153,12 @@ export const adminProviderRoutes = (app, options, done) => {
148
153
  queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
149
154
  maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
150
155
  });
156
+ adaptiveController?.syncProvider(id, {
157
+ adaptive_enabled: isAdaptiveEnabled,
158
+ max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
159
+ queue_timeout_ms: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
160
+ max_queue_size: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
161
+ });
151
162
  tracker?.updateProviderConfig(id, {
152
163
  name: body.name,
153
164
  maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
@@ -191,6 +202,8 @@ export const adminProviderRoutes = (app, options, done) => {
191
202
  fields.queue_timeout_ms = body.queue_timeout_ms;
192
203
  if (body.max_queue_size !== undefined)
193
204
  fields.max_queue_size = body.max_queue_size;
205
+ if (body.adaptive_enabled !== undefined)
206
+ fields.adaptive_enabled = body.adaptive_enabled;
194
207
  if (body.api_key) {
195
208
  fields.api_key = encrypt(body.api_key, getSetting(db, "encryption_key"));
196
209
  fields.api_key_preview = body.api_key.length > API_KEY_PREVIEW_MIN_LENGTH ? `${body.api_key.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${body.api_key.slice(-API_KEY_PREVIEW_PREFIX_LEN)}` : "****";
@@ -208,6 +221,14 @@ export const adminProviderRoutes = (app, options, done) => {
208
221
  maxQueueSize: updated.max_queue_size,
209
222
  });
210
223
  }
224
+ if (body.adaptive_enabled !== undefined || body.max_concurrency !== undefined || body.queue_timeout_ms !== undefined || body.max_queue_size !== undefined) {
225
+ adaptiveController?.syncProvider(id, {
226
+ adaptive_enabled: updated.adaptive_enabled,
227
+ max_concurrency: updated.max_concurrency,
228
+ queue_timeout_ms: updated.queue_timeout_ms,
229
+ max_queue_size: updated.max_queue_size,
230
+ });
231
+ }
211
232
  tracker?.updateProviderConfig(id, {
212
233
  name: body.name ?? existing.name,
213
234
  maxConcurrency: updated.max_concurrency,
@@ -272,8 +293,16 @@ export const adminProviderRoutes = (app, options, done) => {
272
293
  }
273
294
  deleteProvider(db, id);
274
295
  semaphoreManager?.remove(id);
296
+ adaptiveController?.remove(id);
275
297
  tracker?.removeProviderConfig(id);
276
298
  return reply.send({ success: true });
277
299
  });
300
+ app.get("/admin/api/providers/:id/adaptive-status", async (request, reply) => {
301
+ const { id } = request.params;
302
+ const status = adaptiveController?.getStatus(id);
303
+ if (!status)
304
+ return reply.code(HTTP_NOT_FOUND).send({ error: "Not found or adaptive not enabled" });
305
+ return status;
306
+ });
278
307
  done();
279
308
  };
@@ -3,11 +3,13 @@ import Database from "better-sqlite3";
3
3
  import { RetryRuleMatcher } from "../proxy/retry-rules.js";
4
4
  import type { RequestTracker } from "../monitor/request-tracker.js";
5
5
  import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
6
+ import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
6
7
  interface AdminRoutesOptions {
7
8
  db: Database.Database;
8
9
  matcher: RetryRuleMatcher | null;
9
10
  tracker?: RequestTracker;
10
11
  semaphoreManager?: ProviderSemaphoreManager;
12
+ adaptiveController?: AdaptiveConcurrencyController;
11
13
  }
12
14
  export declare const adminRoutes: FastifyPluginCallback<AdminRoutesOptions>;
13
15
  export {};
@@ -21,7 +21,7 @@ export const adminRoutes = (app, options, done) => {
21
21
  app.register(adminSetupRoutes, { db: options.db });
22
22
  app.register(adminAuthPlugin, { db: options.db });
23
23
  app.register(adminLoginRoutes, { db: options.db });
24
- app.register(adminProviderRoutes, { db: options.db, semaphoreManager: options.semaphoreManager, tracker: options.tracker });
24
+ app.register(adminProviderRoutes, { db: options.db, semaphoreManager: options.semaphoreManager, tracker: options.tracker, adaptiveController: options.adaptiveController });
25
25
  app.register(adminMappingRoutes, { db: options.db });
26
26
  app.register(adminGroupRoutes, { db: options.db });
27
27
  app.register(adminScheduleRoutes, { db: options.db });
@@ -0,0 +1,3 @@
1
+ -- 033_add_adaptive_concurrency.sql
2
+ ALTER TABLE providers ADD COLUMN adaptive_enabled INTEGER NOT NULL DEFAULT 0;
3
+ ALTER TABLE providers ADD COLUMN adaptive_min INTEGER NOT NULL DEFAULT 1;
@@ -11,6 +11,7 @@ export interface Provider {
11
11
  max_concurrency: number;
12
12
  queue_timeout_ms: number;
13
13
  max_queue_size: number;
14
+ adaptive_enabled: number;
14
15
  created_at: string;
15
16
  updated_at: string;
16
17
  }
@@ -33,8 +34,9 @@ export declare function createProvider(db: Database.Database, provider: {
33
34
  max_concurrency?: number;
34
35
  queue_timeout_ms?: number;
35
36
  max_queue_size?: number;
37
+ adaptive_enabled?: number;
36
38
  }): string;
37
- export declare function updateProvider(db: Database.Database, id: string, fields: Partial<Pick<Provider, "name" | "api_type" | "base_url" | "api_key" | "api_key_preview" | "models" | "is_active" | "max_concurrency" | "queue_timeout_ms" | "max_queue_size">>): void;
39
+ export declare function updateProvider(db: Database.Database, id: string, fields: Partial<Pick<Provider, "name" | "api_type" | "base_url" | "api_key" | "api_key_preview" | "models" | "is_active" | "max_concurrency" | "queue_timeout_ms" | "max_queue_size" | "adaptive_enabled">>): void;
38
40
  export declare function deleteProvider(db: Database.Database, id: string): void;
39
41
  export declare function getActiveProviderByName(db: Database.Database, name: string): {
40
42
  id: string;
@@ -6,7 +6,7 @@ export const PROVIDER_CONCURRENCY_DEFAULTS = {
6
6
  max_queue_size: 100,
7
7
  };
8
8
  const PROVIDER_FIELDS = new Set([
9
- "name", "api_type", "base_url", "api_key", "api_key_preview", "models", "is_active", "max_concurrency", "queue_timeout_ms", "max_queue_size",
9
+ "name", "api_type", "base_url", "api_key", "api_key_preview", "models", "is_active", "max_concurrency", "queue_timeout_ms", "max_queue_size", "adaptive_enabled",
10
10
  ]);
11
11
  export function getActiveProviders(db, apiType) {
12
12
  return db
@@ -22,8 +22,8 @@ export function getProviderById(db, id) {
22
22
  export function createProvider(db, provider) {
23
23
  const id = randomUUID();
24
24
  const now = new Date().toISOString();
25
- db.prepare(`INSERT INTO providers (id, name, api_type, base_url, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, created_at, updated_at)
26
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, provider.name, provider.api_type, provider.base_url, provider.api_key, provider.api_key_preview ?? null, provider.models ?? "[]", provider.is_active ?? 1, provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency, provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms, provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size, now, now);
25
+ db.prepare(`INSERT INTO providers (id, name, api_type, base_url, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled, created_at, updated_at)
26
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, provider.name, provider.api_type, provider.base_url, provider.api_key, provider.api_key_preview ?? null, provider.models ?? "[]", provider.is_active ?? 1, provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency, provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms, provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size, provider.adaptive_enabled ?? 0, now, now);
27
27
  return id;
28
28
  }
29
29
  export function updateProvider(db, id, fields) {
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { anthropicProxy } from "./proxy/anthropic.js";
20
20
  import { adminRoutes } from "./admin/routes.js";
21
21
  import { RetryRuleMatcher } from "./proxy/retry-rules.js";
22
22
  import { ProviderSemaphoreManager } from "./proxy/semaphore.js";
23
+ import { AdaptiveConcurrencyController } from "./proxy/adaptive-controller.js";
23
24
  import { RequestTracker } from "./monitor/request-tracker.js";
24
25
  import { modelState } from "./proxy/model-state.js";
25
26
  import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
@@ -155,6 +156,8 @@ export async function buildApp(options) {
155
156
  const semaphoreManager = new ProviderSemaphoreManager();
156
157
  const tracker = new RequestTracker({ semaphoreManager, logger: app.log });
157
158
  tracker.startPushInterval();
159
+ const adaptiveController = new AdaptiveConcurrencyController(semaphoreManager, app.log);
160
+ tracker.setAdaptiveController(adaptiveController);
158
161
  // 5h 用量窗口追踪器,启动时自动补齐缺失窗口
159
162
  const usageWindowTracker = new UsageWindowTracker(db);
160
163
  usageWindowTracker.reconcileOnStartup();
@@ -163,7 +166,13 @@ export async function buildApp(options) {
163
166
  // 从 DB 读取已有 provider 的并发配置,初始化信号量管理器和 tracker
164
167
  const allProviders = getAllProviders(db);
165
168
  for (const p of allProviders) {
166
- if (p.max_concurrency > 0) {
169
+ if (p.adaptive_enabled) {
170
+ adaptiveController.init(p.id, { max: p.max_concurrency }, {
171
+ queueTimeoutMs: p.queue_timeout_ms,
172
+ maxQueueSize: p.max_queue_size,
173
+ });
174
+ }
175
+ else if (p.max_concurrency > 0) {
167
176
  semaphoreManager.updateConfig(p.id, {
168
177
  maxConcurrency: p.max_concurrency,
169
178
  queueTimeoutMs: p.queue_timeout_ms,
@@ -187,6 +196,7 @@ export async function buildApp(options) {
187
196
  tracker,
188
197
  usageWindowTracker,
189
198
  sessionTracker,
199
+ adaptiveController,
190
200
  });
191
201
  app.register(anthropicProxy, {
192
202
  db,
@@ -197,8 +207,9 @@ export async function buildApp(options) {
197
207
  tracker,
198
208
  usageWindowTracker,
199
209
  sessionTracker,
210
+ adaptiveController,
200
211
  });
201
- app.register(adminRoutes, { db, matcher, tracker, semaphoreManager });
212
+ app.register(adminRoutes, { db, matcher, tracker, semaphoreManager, adaptiveController });
202
213
  // 前端静态文件服务(生产环境)
203
214
  const frontendDist = path.resolve(process.env.FRONTEND_DIST || path.join(__dirname, "../frontend-dist"));
204
215
  if (existsSync(frontendDist)) {
@@ -2,6 +2,7 @@ import type { ServerResponse } from "node:http";
2
2
  import { StatsAggregator } from "./stats-aggregator.js";
3
3
  import { RuntimeCollector } from "./runtime-collector.js";
4
4
  import type { ProviderSemaphoreManager } from "../proxy/semaphore.js";
5
+ import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
5
6
  import type { ActiveRequest, AttemptSnapshot, ProviderConcurrencySnapshot, RuntimeMetrics, StatsSnapshot } from "./types.js";
6
7
  export interface TrackerLogger {
7
8
  debug(obj: Record<string, unknown>, msg: string): void;
@@ -22,11 +23,13 @@ export declare class RequestTracker {
22
23
  readonly statsAggregator: StatsAggregator;
23
24
  readonly runtimeCollector: RuntimeCollector;
24
25
  private readonly semaphoreManager?;
26
+ private adaptiveController?;
25
27
  constructor(deps?: {
26
28
  semaphoreManager?: ProviderSemaphoreManager;
27
29
  runtimeCollector?: RuntimeCollector;
28
30
  logger?: TrackerLogger;
29
31
  });
32
+ setAdaptiveController(ctrl: AdaptiveConcurrencyController): void;
30
33
  start(req: ActiveRequest): void;
31
34
  /** 轻量级节流推送:流式内容变更后 500ms 内批量广播 */
32
35
  private scheduleStreamContentPush;
@@ -22,12 +22,16 @@ export class RequestTracker {
22
22
  statsAggregator;
23
23
  runtimeCollector;
24
24
  semaphoreManager;
25
+ adaptiveController;
25
26
  constructor(deps) {
26
27
  this.semaphoreManager = deps?.semaphoreManager;
27
28
  this.runtimeCollector = deps?.runtimeCollector ?? new RuntimeCollector();
28
29
  this.statsAggregator = new StatsAggregator();
29
30
  this.logger = deps?.logger;
30
31
  }
32
+ setAdaptiveController(ctrl) {
33
+ this.adaptiveController = ctrl;
34
+ }
31
35
  // --- Core methods ---
32
36
  start(req) {
33
37
  this.activeMap.set(req.id, { ...req });
@@ -142,6 +146,7 @@ export class RequestTracker {
142
146
  const result = [];
143
147
  for (const [providerId, config] of this.providerConfigCache) {
144
148
  const status = this.semaphoreManager.getStatus(providerId);
149
+ const adaptiveState = this.adaptiveController?.getStatus(providerId);
145
150
  result.push({
146
151
  providerId,
147
152
  providerName: config.name,
@@ -150,6 +155,8 @@ export class RequestTracker {
150
155
  queued: status.queued,
151
156
  queueTimeoutMs: config.queueTimeoutMs,
152
157
  maxQueueSize: config.maxQueueSize,
158
+ adaptiveEnabled: adaptiveState !== undefined,
159
+ adaptiveLimit: adaptiveState?.currentLimit,
153
160
  });
154
161
  }
155
162
  return result;
@@ -62,6 +62,8 @@ export interface ProviderConcurrencySnapshot {
62
62
  queued: number;
63
63
  queueTimeoutMs: number;
64
64
  maxQueueSize: number;
65
+ adaptiveEnabled?: boolean;
66
+ adaptiveLimit?: number;
65
67
  }
66
68
  export interface StatsSnapshot {
67
69
  totalRequests: number;
@@ -0,0 +1,42 @@
1
+ import type { ProviderSemaphoreManager } from "./semaphore.js";
2
+ export interface AdaptiveState {
3
+ currentLimit: number;
4
+ probeActive: boolean;
5
+ consecutiveSuccesses: number;
6
+ consecutiveFailures: number;
7
+ cooldownUntil: number;
8
+ }
9
+ interface AdaptiveResult {
10
+ success: boolean;
11
+ statusCode?: number;
12
+ }
13
+ export interface ProviderAdaptiveConfig {
14
+ adaptive_enabled: number;
15
+ max_concurrency: number;
16
+ queue_timeout_ms: number;
17
+ max_queue_size: number;
18
+ }
19
+ export interface ControllerLogger {
20
+ debug(obj: Record<string, unknown>, msg: string): void;
21
+ warn(obj: Record<string, unknown>, msg: string): void;
22
+ }
23
+ export declare class AdaptiveConcurrencyController {
24
+ private semaphoreManager;
25
+ private logger?;
26
+ private readonly entries;
27
+ constructor(semaphoreManager: ProviderSemaphoreManager, logger?: ControllerLogger | undefined);
28
+ init(providerId: string, config: {
29
+ max: number;
30
+ }, semParams: {
31
+ queueTimeoutMs: number;
32
+ maxQueueSize: number;
33
+ }): void;
34
+ remove(providerId: string): void;
35
+ onRequestComplete(providerId: string, result: AdaptiveResult): void;
36
+ getStatus(providerId: string): AdaptiveState | undefined;
37
+ syncProvider(providerId: string, p: ProviderAdaptiveConfig): void;
38
+ private transitionSuccess;
39
+ private transitionFailure;
40
+ private syncToSemaphore;
41
+ }
42
+ export {};
@@ -0,0 +1,130 @@
1
+ const SUCCESS_THRESHOLD = 3;
2
+ const FAILURE_THRESHOLD = 3;
3
+ const DECREASE_STEP = 2;
4
+ const COOLDOWN_MS = 30_000;
5
+ const RATE_LIMIT_STATUS = 429;
6
+ const HALF_DIVISOR = 2;
7
+ const ADAPTIVE_MIN = 1;
8
+ export class AdaptiveConcurrencyController {
9
+ semaphoreManager;
10
+ logger;
11
+ entries = new Map();
12
+ constructor(semaphoreManager, logger) {
13
+ this.semaphoreManager = semaphoreManager;
14
+ this.logger = logger;
15
+ }
16
+ init(providerId, config, semParams) {
17
+ this.entries.set(providerId, {
18
+ state: {
19
+ currentLimit: ADAPTIVE_MIN,
20
+ probeActive: false,
21
+ consecutiveSuccesses: 0,
22
+ consecutiveFailures: 0,
23
+ cooldownUntil: 0,
24
+ },
25
+ max: config.max,
26
+ queueTimeoutMs: semParams.queueTimeoutMs,
27
+ maxQueueSize: semParams.maxQueueSize,
28
+ });
29
+ this.syncToSemaphore(providerId);
30
+ }
31
+ remove(providerId) {
32
+ this.entries.delete(providerId);
33
+ }
34
+ onRequestComplete(providerId, result) {
35
+ const entry = this.entries.get(providerId);
36
+ if (!entry)
37
+ return;
38
+ if (result.success) {
39
+ this.transitionSuccess(providerId, entry);
40
+ }
41
+ else {
42
+ this.transitionFailure(providerId, entry, result.statusCode);
43
+ }
44
+ }
45
+ getStatus(providerId) {
46
+ return this.entries.get(providerId)?.state;
47
+ }
48
+ syncProvider(providerId, p) {
49
+ if (p.adaptive_enabled) {
50
+ const existing = this.entries.get(providerId);
51
+ if (existing) {
52
+ existing.max = p.max_concurrency;
53
+ existing.queueTimeoutMs = p.queue_timeout_ms;
54
+ existing.maxQueueSize = p.max_queue_size;
55
+ existing.state.currentLimit = Math.min(Math.max(existing.state.currentLimit, ADAPTIVE_MIN), existing.max);
56
+ this.syncToSemaphore(providerId);
57
+ }
58
+ else {
59
+ this.init(providerId, { max: p.max_concurrency }, {
60
+ queueTimeoutMs: p.queue_timeout_ms, maxQueueSize: p.max_queue_size,
61
+ });
62
+ }
63
+ }
64
+ else {
65
+ this.remove(providerId);
66
+ // 禁用自适应后恢复信号量到原始 max_concurrency
67
+ this.semaphoreManager.updateConfig(providerId, {
68
+ maxConcurrency: p.max_concurrency,
69
+ queueTimeoutMs: p.queue_timeout_ms,
70
+ maxQueueSize: p.max_queue_size,
71
+ });
72
+ }
73
+ }
74
+ transitionSuccess(providerId, entry) {
75
+ const s = entry.state;
76
+ s.consecutiveSuccesses++;
77
+ s.consecutiveFailures = 0;
78
+ if (Date.now() < s.cooldownUntil)
79
+ return;
80
+ if (s.consecutiveSuccesses >= SUCCESS_THRESHOLD) {
81
+ if (!s.probeActive) {
82
+ s.probeActive = true;
83
+ s.consecutiveSuccesses = 0;
84
+ this.logger?.debug({ providerId, currentLimit: s.currentLimit, action: "probe_open" }, "Adaptive: probe window opened");
85
+ }
86
+ else {
87
+ s.currentLimit = Math.min(s.currentLimit + 1, entry.max);
88
+ s.consecutiveSuccesses = 0;
89
+ this.logger?.debug({ providerId, currentLimit: s.currentLimit, max: entry.max, action: "limit_increased" }, "Adaptive: limit increased by 1");
90
+ }
91
+ this.syncToSemaphore(providerId);
92
+ }
93
+ }
94
+ transitionFailure(providerId, entry, statusCode) {
95
+ const s = entry.state;
96
+ s.consecutiveFailures++;
97
+ s.consecutiveSuccesses = 0;
98
+ if (statusCode === RATE_LIMIT_STATUS) {
99
+ const prevLimit = s.currentLimit;
100
+ s.currentLimit = Math.max(Math.floor(s.currentLimit / HALF_DIVISOR), ADAPTIVE_MIN);
101
+ s.probeActive = false;
102
+ s.cooldownUntil = Date.now() + COOLDOWN_MS;
103
+ s.consecutiveFailures = 0;
104
+ this.syncToSemaphore(providerId);
105
+ this.logger?.warn({ providerId, prevLimit, newLimit: s.currentLimit, cooldownMs: COOLDOWN_MS, action: "rate_limit_backoff" }, "Adaptive: 429 rate limit, halved concurrency and entered cooldown");
106
+ }
107
+ else if (s.consecutiveFailures >= FAILURE_THRESHOLD) {
108
+ const prevLimit = s.currentLimit;
109
+ s.currentLimit = Math.max(s.currentLimit - DECREASE_STEP, ADAPTIVE_MIN);
110
+ s.probeActive = false;
111
+ s.consecutiveFailures = 0;
112
+ this.syncToSemaphore(providerId);
113
+ this.logger?.warn({ providerId, prevLimit, newLimit: s.currentLimit, action: "failure_backoff" }, "Adaptive: sustained failures, decreased concurrency");
114
+ }
115
+ }
116
+ syncToSemaphore(providerId) {
117
+ const entry = this.entries.get(providerId);
118
+ if (!entry)
119
+ return;
120
+ // probeActive 时额外加 1 个探针槽位,但不超过 max
121
+ const effectiveLimit = entry.state.probeActive
122
+ ? Math.min(Math.max(entry.state.currentLimit + 1, ADAPTIVE_MIN), entry.max)
123
+ : Math.max(entry.state.currentLimit, ADAPTIVE_MIN);
124
+ this.semaphoreManager.updateConfig(providerId, {
125
+ maxConcurrency: effectiveLimit,
126
+ queueTimeoutMs: entry.queueTimeoutMs,
127
+ maxQueueSize: entry.maxQueueSize,
128
+ });
129
+ }
130
+ }
@@ -4,6 +4,7 @@ import { RetryRuleMatcher } from "./retry-rules.js";
4
4
  import { ProviderSemaphoreManager } from "./semaphore.js";
5
5
  import type { RequestTracker } from "../monitor/request-tracker.js";
6
6
  import type { UsageWindowTracker } from "./usage-window-tracker.js";
7
+ import type { AdaptiveConcurrencyController } from "./adaptive-controller.js";
7
8
  export interface AnthropicProxyOptions {
8
9
  db: Database.Database;
9
10
  streamTimeoutMs: number;
@@ -13,5 +14,6 @@ export interface AnthropicProxyOptions {
13
14
  tracker?: RequestTracker;
14
15
  usageWindowTracker?: UsageWindowTracker;
15
16
  sessionTracker?: import("./loop-prevention/session-tracker.js").SessionTracker;
17
+ adaptiveController?: AdaptiveConcurrencyController;
16
18
  }
17
19
  export declare const anthropicProxy: FastifyPluginCallback<AnthropicProxyOptions>;
@@ -18,8 +18,8 @@ const ANTHROPIC_ERROR_TYPE = {
18
18
  };
19
19
  const anthropicErrors = createErrorFormatter((kind, message) => ({ type: "error", error: { type: ANTHROPIC_ERROR_TYPE[kind], message } }));
20
20
  const anthropicProxyRaw = (app, opts, done) => {
21
- const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker, sessionTracker } = opts;
22
- const orchestrator = createOrchestrator(semaphoreManager, tracker);
21
+ const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker, sessionTracker, adaptiveController } = opts;
22
+ const orchestrator = createOrchestrator(semaphoreManager, tracker, adaptiveController);
23
23
  app.post(MESSAGES_PATH, async (request, reply) => {
24
24
  if (!orchestrator) {
25
25
  const body = request.body;
@@ -4,6 +4,7 @@ import { RetryRuleMatcher } from "./retry-rules.js";
4
4
  import { ProviderSemaphoreManager } from "./semaphore.js";
5
5
  import type { RequestTracker } from "../monitor/request-tracker.js";
6
6
  import type { UsageWindowTracker } from "./usage-window-tracker.js";
7
+ import type { AdaptiveConcurrencyController } from "./adaptive-controller.js";
7
8
  export interface OpenaiProxyOptions {
8
9
  db: Database.Database;
9
10
  streamTimeoutMs: number;
@@ -13,5 +14,6 @@ export interface OpenaiProxyOptions {
13
14
  tracker?: RequestTracker;
14
15
  usageWindowTracker?: UsageWindowTracker;
15
16
  sessionTracker?: import("./loop-prevention/session-tracker.js").SessionTracker;
17
+ adaptiveController?: AdaptiveConcurrencyController;
16
18
  }
17
19
  export declare const openaiProxy: FastifyPluginCallback<OpenaiProxyOptions>;
@@ -24,8 +24,8 @@ function sendError(reply, e) {
24
24
  return reply.code(e.statusCode).send(e.body);
25
25
  }
26
26
  const openaiProxyRaw = (app, opts, done) => {
27
- const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker, sessionTracker } = opts;
28
- const orchestrator = createOrchestrator(semaphoreManager, tracker);
27
+ const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker, sessionTracker, adaptiveController } = opts;
28
+ const orchestrator = createOrchestrator(semaphoreManager, tracker, adaptiveController);
29
29
  app.post(CHAT_COMPLETIONS_PATH, async (request, reply) => {
30
30
  if (!orchestrator) {
31
31
  const body = request.body;
@@ -7,6 +7,7 @@ import type { SemaphoreScope } from "./scope.js";
7
7
  import type { TrackerScope } from "./scope.js";
8
8
  import type { ProviderSemaphoreManager } from "./semaphore.js";
9
9
  import type { RequestTracker } from "../monitor/request-tracker.js";
10
+ import type { AdaptiveConcurrencyController } from "./adaptive-controller.js";
10
11
  export interface OrchestratorConfig {
11
12
  resolved: Target;
12
13
  provider: {
@@ -40,13 +41,14 @@ export interface HandleContext {
40
41
  * 工厂函数,消除 openai/anthropic 创建 orchestrator 的重复代码。
41
42
  * 两个 provider 的创建逻辑完全一致。
42
43
  */
43
- export declare function createOrchestrator(semaphoreManager?: ProviderSemaphoreManager, tracker?: RequestTracker): ProxyOrchestrator | undefined;
44
+ export declare function createOrchestrator(semaphoreManager?: ProviderSemaphoreManager, tracker?: RequestTracker, adaptiveController?: AdaptiveConcurrencyController): ProxyOrchestrator | undefined;
44
45
  export declare class ProxyOrchestrator {
45
46
  private deps;
46
47
  constructor(deps: {
47
48
  semaphoreScope: SemaphoreScope;
48
49
  trackerScope: TrackerScope;
49
50
  resilience: ResilienceLayer;
51
+ adaptiveController?: AdaptiveConcurrencyController;
50
52
  });
51
53
  handle(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "anthropic", config: OrchestratorConfig, ctx?: HandleContext): Promise<ResilienceResult>;
52
54
  private buildActiveRequest;
@@ -1,3 +1,4 @@
1
+ import { ProviderSwitchNeeded } from "./types.js";
1
2
  import { ResilienceLayer as ResilienceLayerClass } from "./resilience.js";
2
3
  import { SemaphoreScope as SemaphoreScopeClass } from "./scope.js";
3
4
  import { TrackerScope as TrackerScopeClass } from "./scope.js";
@@ -7,12 +8,12 @@ const DEFAULT_FAILOVER_THRESHOLD = 400;
7
8
  * 工厂函数,消除 openai/anthropic 创建 orchestrator 的重复代码。
8
9
  * 两个 provider 的创建逻辑完全一致。
9
10
  */
10
- export function createOrchestrator(semaphoreManager, tracker) {
11
+ export function createOrchestrator(semaphoreManager, tracker, adaptiveController) {
11
12
  const semaphoreScope = semaphoreManager ? new SemaphoreScopeClass(semaphoreManager) : undefined;
12
13
  const trackerScope = tracker ? new TrackerScopeClass(tracker) : undefined;
13
14
  if (!semaphoreScope || !trackerScope)
14
15
  return undefined;
15
- return new ProxyOrchestrator({ semaphoreScope, trackerScope, resilience: new ResilienceLayerClass() });
16
+ return new ProxyOrchestrator({ semaphoreScope, trackerScope, resilience: new ResilienceLayerClass(), adaptiveController });
16
17
  }
17
18
  export class ProxyOrchestrator {
18
19
  deps;
@@ -20,24 +21,37 @@ export class ProxyOrchestrator {
20
21
  this.deps = deps;
21
22
  }
22
23
  async handle(request, reply, apiType, config, ctx) {
24
+ const providerId = config.provider.id;
23
25
  const trackerReq = this.buildActiveRequest(request, config, apiType);
24
- const result = await this.deps.trackerScope.track(trackerReq, () => this.deps.semaphoreScope.withSlot(config.provider.id, this.createAbortSignal(request), () => {
25
- trackerReq.queued = true;
26
- this.deps.trackerScope.markQueued(trackerReq.id, true);
27
- }, () => {
28
- if (trackerReq.queued) {
29
- trackerReq.queued = false;
30
- this.deps.trackerScope.markQueued(trackerReq.id, false);
26
+ try {
27
+ const result = await this.deps.trackerScope.track(trackerReq, () => this.deps.semaphoreScope.withSlot(providerId, this.createAbortSignal(request), () => {
28
+ trackerReq.queued = true;
29
+ this.deps.trackerScope.markQueued(trackerReq.id, true);
30
+ }, () => {
31
+ if (trackerReq.queued) {
32
+ trackerReq.queued = false;
33
+ this.deps.trackerScope.markQueued(trackerReq.id, false);
34
+ }
35
+ return this.executeResilience(config, ctx);
36
+ }, config.concurrencyOverride), (result) => this.extractTrackStatus(result), (result) => result.attempts.map(a => ({
37
+ statusCode: a.statusCode,
38
+ error: a.error,
39
+ latencyMs: a.latencyMs,
40
+ providerId: a.target.provider_id,
41
+ })));
42
+ const { status, statusCode } = this.extractTrackStatus(result);
43
+ this.deps.adaptiveController?.onRequestComplete(providerId, { success: status === "completed", statusCode });
44
+ this.sendResponse(reply, result.result, ctx);
45
+ return result;
46
+ }
47
+ catch (e) {
48
+ if (e instanceof ProviderSwitchNeeded) {
49
+ const lastResult = e.lastResult;
50
+ const statusCode = lastResult && "statusCode" in lastResult ? lastResult.statusCode : undefined;
51
+ this.deps.adaptiveController?.onRequestComplete(providerId, { success: false, statusCode });
31
52
  }
32
- return this.executeResilience(config, ctx);
33
- }, config.concurrencyOverride), (result) => this.extractTrackStatus(result), (result) => result.attempts.map(a => ({
34
- statusCode: a.statusCode,
35
- error: a.error,
36
- latencyMs: a.latencyMs,
37
- providerId: a.target.provider_id,
38
- })));
39
- this.sendResponse(reply, result.result, ctx);
40
- return result;
53
+ throw e;
54
+ }
41
55
  }
42
56
  buildActiveRequest(request, config, apiType) {
43
57
  return {
@@ -1 +1 @@
1
- import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-D4qBQ0nA.js";var s=[`data-size`],c=n({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(n){let c=n;return(l,u)=>(a(),e(`div`,{"data-slot":`card`,"data-size":n.size,class:t(r(o)(`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))},[i(l.$slots,`default`)],10,s))}}),l=n({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-content`,class:t(r(o)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[i(n.$slots,`default`)],2))}});export{c as n,l as t};
1
+ import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-N59D1BGa.js";var s=[`data-size`],c=n({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(n){let c=n;return(l,u)=>(a(),e(`div`,{"data-slot":`card`,"data-size":n.size,class:t(r(o)(`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))},[i(l.$slots,`default`)],10,s))}}),l=n({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-content`,class:t(r(o)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[i(n.$slots,`default`)],2))}});export{c as n,l as t};
@@ -1 +1 @@
1
- import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-D4qBQ0nA.js";var s=n({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-header`,class:t(r(o)(`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))},[i(n.$slots,`default`)],2))}}),c=n({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-title`,class:t(r(o)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[i(n.$slots,`default`)],2))}});export{s as n,c as t};
1
+ import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-N59D1BGa.js";var s=n({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-header`,class:t(r(o)(`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))},[i(n.$slots,`default`)],2))}}),c=n({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-title`,class:t(r(o)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[i(n.$slots,`default`)],2))}});export{s as n,c as t};