llm-simple-router 0.11.17 → 0.11.18

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 (108) hide show
  1. package/dist/admin/retry-rules.js +51 -0
  2. package/dist/db/index.d.ts +2 -0
  3. package/dist/db/index.js +1 -0
  4. package/dist/db/migrations/049_add_provider_isolation_and_matchers.sql +22 -0
  5. package/dist/db/retry-rules.d.ts +5 -1
  6. package/dist/db/retry-rules.js +3 -3
  7. package/dist/db/upstream-error-logs.d.ts +29 -0
  8. package/dist/db/upstream-error-logs.js +40 -0
  9. package/dist/proxy/handler/failover-loop.js +31 -0
  10. package/dist/proxy/orchestration/body-matcher.d.ts +24 -0
  11. package/dist/proxy/orchestration/body-matcher.js +89 -0
  12. package/dist/proxy/orchestration/orchestrator.js +2 -1
  13. package/dist/proxy/orchestration/resilience.d.ts +2 -0
  14. package/dist/proxy/orchestration/resilience.js +3 -3
  15. package/dist/proxy/orchestration/retry-rules.d.ts +4 -2
  16. package/dist/proxy/orchestration/retry-rules.js +51 -12
  17. package/dist/proxy/transport/transport-fn.js +1 -1
  18. package/frontend-dist/assets/{CardContent-DCQR2368.js → CardContent-DqY4C5hv.js} +1 -1
  19. package/frontend-dist/assets/{CardTitle-BhMM67Wj.js → CardTitle-CIW3zaxJ.js} +1 -1
  20. package/frontend-dist/assets/CascadingModelSelect-Bt4Pp5CS.js +1 -0
  21. package/frontend-dist/assets/{Checkbox-B5YofFYK.js → Checkbox-Dtzyj_Mx.js} +1 -1
  22. package/frontend-dist/assets/{CollapsibleContent-Bn1QXK6j.js → CollapsibleContent-DrIK1N9f.js} +1 -1
  23. package/frontend-dist/assets/CollapsibleTrigger-0C6Isfde.js +1 -0
  24. package/frontend-dist/assets/{Dashboard-CAMubYhi.js → Dashboard-iz-xysfd.js} +2 -2
  25. package/frontend-dist/assets/{Input-D1Dgx5aQ.js → Input-CvlJOI6Z.js} +1 -1
  26. package/frontend-dist/assets/{Label-BmVN9gSu.js → Label-CwsQ60Yf.js} +1 -1
  27. package/frontend-dist/assets/{Login-BrGvgfxl.js → Login-DSjsqE-c.js} +1 -1
  28. package/frontend-dist/assets/Logs-9rFY2MsD.js +1 -0
  29. package/frontend-dist/assets/MappingEntryEditor-DPOCfF3z.js +1 -0
  30. package/frontend-dist/assets/ModelMappings-B3FeIPnC.js +1 -0
  31. package/frontend-dist/assets/Monitor-IuX9QMJH.js +1 -0
  32. package/frontend-dist/assets/{Providers-MrtjZEAT.js → Providers-CGKROJ83.js} +1 -1
  33. package/frontend-dist/assets/ProxyEnhancement-Sc3CQpkn.js +1 -0
  34. package/frontend-dist/assets/{QuickSetup-BRvO-y_3.js → QuickSetup-C2kDKJ7l.js} +1 -1
  35. package/frontend-dist/assets/RetryRules-ekDQR17j.js +1 -0
  36. package/frontend-dist/assets/RouterKeys-BYDmosW1.js +1 -0
  37. package/frontend-dist/assets/{RovingFocusItem-Dzeu5KRT.js → RovingFocusItem-DntStLzY.js} +1 -1
  38. package/frontend-dist/assets/Schedules-Sdzde0R2.js +1 -0
  39. package/frontend-dist/assets/{Settings-BuV0EjD9.js → Settings-CvOesX4a.js} +1 -1
  40. package/frontend-dist/assets/{Setup-nOtjStYO.js → Setup-DhLn54zX.js} +1 -1
  41. package/frontend-dist/assets/{Switch-CwKiYHl-.js → Switch-D8VfSEnf.js} +1 -1
  42. package/frontend-dist/assets/{TooltipTrigger-CGUpAsNa.js → TooltipTrigger-BKICqqnF.js} +1 -1
  43. package/frontend-dist/assets/{TransformRulesForm-Bk4hpVsX.js → TransformRulesForm-Ch_KBFUw.js} +1 -1
  44. package/frontend-dist/assets/UnifiedRequestDialog-Bst4BwB4.js +3 -0
  45. package/frontend-dist/assets/{VisuallyHiddenInput-CJWO0qnA.js → VisuallyHiddenInput-DHeeXXfG.js} +1 -1
  46. package/frontend-dist/assets/{button-DyBccemD.js → button-Dwb0WM4k.js} +2 -2
  47. package/frontend-dist/assets/{copy-pLU8Wg9s.js → copy-z2wnatoS.js} +1 -1
  48. package/frontend-dist/assets/{dialog-DTyTrRWK.js → dialog-7EH9lnXi.js} +1 -1
  49. package/frontend-dist/assets/index-BaNw4aag.css +1 -0
  50. package/frontend-dist/assets/{index-DHx0uy1i.js → index-CI2C4oZ_.js} +2 -2
  51. package/frontend-dist/assets/logs-DAfOmnUs.js +1 -0
  52. package/frontend-dist/assets/logs-DkiQi5ER.js +1 -0
  53. package/frontend-dist/assets/{model-patches-CYdOvNIM.js → model-patches-z92RV8oY.js} +1 -1
  54. package/frontend-dist/assets/plus-CJe5xtMJ.js +1 -0
  55. package/frontend-dist/assets/{providers-BjaFz2uN.js → providers-ChuD67aW.js} +1 -1
  56. package/frontend-dist/assets/{providers-Djvbh2Pk.js → providers-DEOmviin.js} +1 -1
  57. package/frontend-dist/assets/retryRules-CEGYoM2X.js +1 -0
  58. package/frontend-dist/assets/retryRules-DGg26acb.js +3 -0
  59. package/frontend-dist/assets/routerKeys-C4oCPJrT.js +1 -0
  60. package/frontend-dist/assets/routerKeys-CyZ4L-1h.js +1 -0
  61. package/frontend-dist/assets/{sparkles-BUUu02Fq.js → sparkles-4KPHhHvW.js} +1 -1
  62. package/frontend-dist/assets/{trash-2-2tgBiASz.js → trash-2-l1HwaMsK.js} +1 -1
  63. package/frontend-dist/assets/{useLogRetention-DnRHF6ie.js → useLogRetention-BeiWQy2K.js} +1 -1
  64. package/frontend-dist/index.html +3 -3
  65. package/package.json +1 -1
  66. package/frontend-dist/assets/CascadingModelSelect-CTodFvdd.js +0 -1
  67. package/frontend-dist/assets/CollapsibleTrigger-BBkiWl_d.js +0 -1
  68. package/frontend-dist/assets/Logs-PrF4ijTc.js +0 -1
  69. package/frontend-dist/assets/MappingEntryEditor-C3r7weks.js +0 -1
  70. package/frontend-dist/assets/ModelMappings-DfuSP8wW.js +0 -1
  71. package/frontend-dist/assets/Monitor-Db_yxvjc.js +0 -1
  72. package/frontend-dist/assets/ProxyEnhancement-sFtoczuy.js +0 -1
  73. package/frontend-dist/assets/RetryRules-D722cpmL.js +0 -1
  74. package/frontend-dist/assets/RouterKeys-sY5Y77qI.js +0 -1
  75. package/frontend-dist/assets/Schedules-De0ouYPh.js +0 -1
  76. package/frontend-dist/assets/UnifiedRequestDialog-DPocBUjz.js +0 -3
  77. package/frontend-dist/assets/index-BowCJXHo.css +0 -1
  78. package/frontend-dist/assets/logs-C8j2wv9U.js +0 -1
  79. package/frontend-dist/assets/logs-DXEeXyQL.js +0 -1
  80. package/frontend-dist/assets/retryRules-Btt-s8hs.js +0 -1
  81. package/frontend-dist/assets/retryRules-Cnh9jDD4.js +0 -1
  82. package/frontend-dist/assets/routerKeys-BKIv9voD.js +0 -1
  83. package/frontend-dist/assets/routerKeys-DHqew7e3.js +0 -1
  84. package/frontend-dist/assets/useClipboard-BnExJY_c.js +0 -1
  85. /package/frontend-dist/assets/{common-DpEjrxgC.js → common-BCe8fK5N.js} +0 -0
  86. /package/frontend-dist/assets/{common-Cg4OGISS.js → common-CdzdKRsV.js} +0 -0
  87. /package/frontend-dist/assets/{dashboard-oYrGiYFH.js → dashboard-C0XEgNnb.js} +0 -0
  88. /package/frontend-dist/assets/{dashboard-DxQj2qDW.js → dashboard-uBv6TkGn.js} +0 -0
  89. /package/frontend-dist/assets/{login-Cqit6dLn.js → login-BozjQrft.js} +0 -0
  90. /package/frontend-dist/assets/{login-COgZiZU0.js → login-DXMVxesL.js} +0 -0
  91. /package/frontend-dist/assets/{mappings-CIi5L6vx.js → mappings-B6NCfNio.js} +0 -0
  92. /package/frontend-dist/assets/{mappings-DK14Q480.js → mappings-yo-RxHK6.js} +0 -0
  93. /package/frontend-dist/assets/{monitor-BrKGZyOA.js → monitor-CcfwHbJF.js} +0 -0
  94. /package/frontend-dist/assets/{monitor-sNuyagci.js → monitor-XNX6LZGC.js} +0 -0
  95. /package/frontend-dist/assets/{proxyEnhancement-DsQ6_BKy.js → proxyEnhancement-BC3eVKDp.js} +0 -0
  96. /package/frontend-dist/assets/{proxyEnhancement-Caq4cKe6.js → proxyEnhancement-Dq47MAAI.js} +0 -0
  97. /package/frontend-dist/assets/{quickSetup-BL0txMvb.js → quickSetup-C8jt6VzA.js} +0 -0
  98. /package/frontend-dist/assets/{quickSetup-CvR1GTCW.js → quickSetup-tMWsXvo3.js} +0 -0
  99. /package/frontend-dist/assets/{requestDetail-DDzGbK-Q.js → requestDetail-BI1tm09T.js} +0 -0
  100. /package/frontend-dist/assets/{requestDetail-C6o1ku8x.js → requestDetail-dJ1P4rgQ.js} +0 -0
  101. /package/frontend-dist/assets/{schedules-Chof0Byr.js → schedules-ByzpVhl2.js} +0 -0
  102. /package/frontend-dist/assets/{schedules-BdCs4P0W.js → schedules-tSjne6S-.js} +0 -0
  103. /package/frontend-dist/assets/{settings-DheKiB0E.js → settings-CveTb9z9.js} +0 -0
  104. /package/frontend-dist/assets/{settings-BAfdizNX.js → settings-DQqli04G.js} +0 -0
  105. /package/frontend-dist/assets/{setup-AGblmz9n.js → setup-BKw9vkV3.js} +0 -0
  106. /package/frontend-dist/assets/{setup-CghpqjMU.js → setup-D9yk-VSM.js} +0 -0
  107. /package/frontend-dist/assets/{sidebar-CpYOxTtl.js → sidebar-HuUg5Wsk.js} +0 -0
  108. /package/frontend-dist/assets/{sidebar-BMFaYdll.js → sidebar-T-fjy0Pb.js} +0 -0
@@ -99,6 +99,8 @@ const CreateRetryRuleSchema = Type.Object({
99
99
  retry_delay_ms: Type.Optional(Type.Number({ minimum: 100 })),
100
100
  max_retries: Type.Optional(Type.Number({ minimum: 0, maximum: 100 })),
101
101
  max_delay_ms: Type.Optional(Type.Number({ minimum: 100 })),
102
+ provider_id: Type.Optional(Type.Union([Type.String(), Type.Null()])),
103
+ body_matchers: Type.Optional(Type.Union([Type.String(), Type.Null()])),
102
104
  });
103
105
  const UpdateRetryRuleSchema = Type.Object({
104
106
  name: Type.Optional(Type.String({ minLength: 1 })),
@@ -109,6 +111,8 @@ const UpdateRetryRuleSchema = Type.Object({
109
111
  retry_delay_ms: Type.Optional(Type.Number({ minimum: 100 })),
110
112
  max_retries: Type.Optional(Type.Number({ minimum: 0, maximum: 100 })),
111
113
  max_delay_ms: Type.Optional(Type.Number({ minimum: 100 })),
114
+ provider_id: Type.Optional(Type.Union([Type.String(), Type.Null()])),
115
+ body_matchers: Type.Optional(Type.Union([Type.String(), Type.Null()])),
112
116
  });
113
117
  function validateBodyPattern(pattern) {
114
118
  try {
@@ -119,6 +123,32 @@ function validateBodyPattern(pattern) {
119
123
  return "Invalid body_pattern regex";
120
124
  }
121
125
  }
126
+ /** 校验 body_matchers JSON 格式:必须是数组,每项含 path/operator/value */
127
+ function validateBodyMatchers(bodyMatchers) {
128
+ if (bodyMatchers == null || bodyMatchers === "")
129
+ return null;
130
+ let parsed;
131
+ try {
132
+ parsed = JSON.parse(bodyMatchers);
133
+ }
134
+ catch {
135
+ throw new Error("body_matchers must be valid JSON");
136
+ }
137
+ if (!Array.isArray(parsed))
138
+ throw new Error("body_matchers must be a JSON array");
139
+ const VALID_OPERATORS = ["equals", "contains", "exists"];
140
+ for (const item of parsed) {
141
+ if (typeof item !== "object" || item === null)
142
+ throw new Error("Each body_matcher must be an object");
143
+ if (typeof item.path !== "string")
144
+ throw new Error("body_matcher.path is required and must be a string");
145
+ if (!VALID_OPERATORS.includes(item.operator))
146
+ throw new Error("body_matcher.operator must be equals, contains, or exists");
147
+ if (item.operator !== "exists" && typeof item.value !== "string")
148
+ throw new Error("body_matcher.value is required for equals/contains operators");
149
+ }
150
+ return bodyMatchers;
151
+ }
122
152
  // ---------- AI Retry Rule Generation Helpers ----------
123
153
  const MAX_RESPONSE_CHARS = 4000;
124
154
  const STATUS_CODE_MIN = 100;
@@ -246,6 +276,14 @@ export const adminRetryRuleRoutes = (app, options, done) => {
246
276
  if (regexError) {
247
277
  return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.INVALID_REGEX, regexError));
248
278
  }
279
+ let bodyMatchers;
280
+ try {
281
+ bodyMatchers = validateBodyMatchers(body.body_matchers);
282
+ }
283
+ catch (e) {
284
+ const msg = e instanceof Error ? e.message : "Invalid body_matchers";
285
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, msg));
286
+ }
249
287
  const id = createRetryRule(db, {
250
288
  name: body.name,
251
289
  status_code: body.status_code,
@@ -255,6 +293,8 @@ export const adminRetryRuleRoutes = (app, options, done) => {
255
293
  retry_delay_ms: body.retry_delay_ms ?? DEFAULT_RETRY_DELAY_MS,
256
294
  max_retries: body.max_retries ?? DEFAULT_MAX_RETRIES,
257
295
  max_delay_ms: body.max_delay_ms ?? DEFAULT_MAX_DELAY_MS,
296
+ provider_id: body.provider_id || null,
297
+ body_matchers: bodyMatchers,
258
298
  });
259
299
  stateRegistry?.refreshRetryRules();
260
300
  return reply.code(HTTP_CREATED).send({ id });
@@ -284,6 +324,17 @@ export const adminRetryRuleRoutes = (app, options, done) => {
284
324
  fields.max_retries = body.max_retries;
285
325
  if (body.max_delay_ms !== undefined)
286
326
  fields.max_delay_ms = body.max_delay_ms;
327
+ if (body.provider_id !== undefined)
328
+ fields.provider_id = body.provider_id || null;
329
+ if (body.body_matchers !== undefined) {
330
+ try {
331
+ fields.body_matchers = validateBodyMatchers(body.body_matchers);
332
+ }
333
+ catch (e) {
334
+ const msg = e instanceof Error ? e.message : "Invalid body_matchers";
335
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, msg));
336
+ }
337
+ }
287
338
  updateRetryRule(db, id, fields);
288
339
  stateRegistry?.refreshRetryRules();
289
340
  return reply.send({ success: true });
@@ -24,3 +24,5 @@ export { getSchedulesByGroup, getActiveSchedulesForGroup, getScheduleById, getAl
24
24
  export type { Schedule } from "./schedules.js";
25
25
  export { collectDbSizeInfo, runSizeBasedCleanup, scheduleDbSizeMonitor, } from "./db-size-monitor.js";
26
26
  export type { DbSizeInfo, SizeThresholds, DbSizeMonitorHandle } from "./db-size-monitor.js";
27
+ export { logUpstreamError, extractErrorInfo, cleanUpstreamErrorLogs, } from "./upstream-error-logs.js";
28
+ export type { UpstreamErrorLog } from "./upstream-error-logs.js";
package/dist/db/index.js CHANGED
@@ -152,3 +152,4 @@ export { insertWindow, getLatestWindow, getLatestWindowByProvider, getWindowsInR
152
152
  export { getModelContextWindowOverride, getModelInfoForProvider, setModelInfoForProvider, deleteAllModelInfoForProvider, getAllModelInfo, } from "./model-info.js";
153
153
  export { getSchedulesByGroup, getActiveSchedulesForGroup, getScheduleById, getAllSchedules, createSchedule, updateSchedule, deleteSchedule, deleteSchedulesByGroup, } from "./schedules.js";
154
154
  export { collectDbSizeInfo, runSizeBasedCleanup, scheduleDbSizeMonitor, } from "./db-size-monitor.js";
155
+ export { logUpstreamError, extractErrorInfo, cleanUpstreamErrorLogs, } from "./upstream-error-logs.js";
@@ -0,0 +1,22 @@
1
+ -- BG1: Provider isolation for retry rules + upstream error logs table
2
+
3
+ ALTER TABLE retry_rules ADD COLUMN provider_id TEXT NULL DEFAULT NULL;
4
+ ALTER TABLE retry_rules ADD COLUMN body_matchers TEXT NULL DEFAULT NULL;
5
+
6
+ CREATE TABLE upstream_error_logs (
7
+ id TEXT PRIMARY KEY,
8
+ request_log_id TEXT REFERENCES request_logs(id) ON DELETE SET NULL,
9
+ provider_id TEXT NOT NULL,
10
+ backend_model TEXT NOT NULL,
11
+ status_code INTEGER NOT NULL,
12
+ error_type TEXT,
13
+ error_message TEXT,
14
+ client_agent_type TEXT NOT NULL DEFAULT 'unknown',
15
+ router_key_id TEXT,
16
+ session_id TEXT,
17
+ retry_count INTEGER NOT NULL DEFAULT 0,
18
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
19
+ );
20
+ CREATE INDEX idx_upstream_error_logs_time ON upstream_error_logs(created_at);
21
+ CREATE INDEX idx_upstream_error_logs_provider ON upstream_error_logs(provider_id, created_at);
22
+ CREATE INDEX idx_upstream_error_logs_status ON upstream_error_logs(status_code, created_at);
@@ -10,6 +10,8 @@ export interface RetryRule {
10
10
  retry_delay_ms: number;
11
11
  max_retries: number;
12
12
  max_delay_ms: number;
13
+ provider_id: string | null;
14
+ body_matchers: string | null;
13
15
  }
14
16
  export declare function getActiveRetryRules(db: Database.Database): RetryRule[];
15
17
  export declare function getAllRetryRules(db: Database.Database): RetryRule[];
@@ -22,7 +24,9 @@ export declare function createRetryRule(db: Database.Database, rule: {
22
24
  retry_delay_ms?: number;
23
25
  max_retries?: number;
24
26
  max_delay_ms?: number;
27
+ provider_id?: string | null;
28
+ body_matchers?: string | null;
25
29
  }): string;
26
- export declare function updateRetryRule(db: Database.Database, id: string, fields: Partial<Pick<RetryRule, "name" | "status_code" | "body_pattern" | "is_active" | "retry_strategy" | "retry_delay_ms" | "max_retries" | "max_delay_ms">>): void;
30
+ export declare function updateRetryRule(db: Database.Database, id: string, fields: Partial<Pick<RetryRule, "name" | "status_code" | "body_pattern" | "is_active" | "retry_strategy" | "retry_delay_ms" | "max_retries" | "max_delay_ms" | "provider_id" | "body_matchers">>): void;
27
31
  export declare function deleteRetryRule(db: Database.Database, id: string): void;
28
32
  export declare function getRetryRuleById(db: Database.Database, id: string): RetryRule | undefined;
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import { buildUpdateQuery, deleteById } from "./helpers.js";
3
- const RETRY_FIELDS = new Set(["name", "status_code", "body_pattern", "is_active", "retry_strategy", "retry_delay_ms", "max_retries", "max_delay_ms"]);
3
+ const RETRY_FIELDS = new Set(["name", "status_code", "body_pattern", "is_active", "retry_strategy", "retry_delay_ms", "max_retries", "max_delay_ms", "provider_id", "body_matchers"]);
4
4
  const DEFAULT_RETRY_DELAY_MS = 5000;
5
5
  const DEFAULT_MAX_RETRIES = 10;
6
6
  const DEFAULT_MAX_DELAY_MS = 60000;
@@ -17,8 +17,8 @@ export function getAllRetryRules(db) {
17
17
  export function createRetryRule(db, rule) {
18
18
  const id = randomUUID();
19
19
  const now = new Date().toISOString();
20
- db.prepare(`INSERT INTO retry_rules (id, name, status_code, body_pattern, is_active, created_at, retry_strategy, retry_delay_ms, max_retries, max_delay_ms)
21
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, rule.name, rule.status_code, rule.body_pattern, rule.is_active ?? 1, now, rule.retry_strategy ?? "exponential", rule.retry_delay_ms ?? DEFAULT_RETRY_DELAY_MS, rule.max_retries ?? DEFAULT_MAX_RETRIES, rule.max_delay_ms ?? DEFAULT_MAX_DELAY_MS);
20
+ db.prepare(`INSERT INTO retry_rules (id, name, status_code, body_pattern, is_active, created_at, retry_strategy, retry_delay_ms, max_retries, max_delay_ms, provider_id, body_matchers)
21
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, rule.name, rule.status_code, rule.body_pattern, rule.is_active ?? 1, now, rule.retry_strategy ?? "exponential", rule.retry_delay_ms ?? DEFAULT_RETRY_DELAY_MS, rule.max_retries ?? DEFAULT_MAX_RETRIES, rule.max_delay_ms ?? DEFAULT_MAX_DELAY_MS, rule.provider_id ?? null, rule.body_matchers ?? null);
22
22
  return id;
23
23
  }
24
24
  export function updateRetryRule(db, id, fields) {
@@ -0,0 +1,29 @@
1
+ import Database from "better-sqlite3";
2
+ export interface UpstreamErrorLog {
3
+ id: string;
4
+ request_log_id: string | null;
5
+ provider_id: string;
6
+ backend_model: string;
7
+ status_code: number;
8
+ error_type: string | null;
9
+ error_message: string | null;
10
+ client_agent_type: string;
11
+ router_key_id: string | null;
12
+ session_id: string | null;
13
+ retry_count: number;
14
+ created_at: string;
15
+ }
16
+ /** 写入单条上游错误记录 */
17
+ export declare function logUpstreamError(db: Database.Database, entry: Omit<UpstreamErrorLog, "id" | "created_at">): void;
18
+ /**
19
+ * 从上游响应体提取错误信息。
20
+ * 优先 error.type,其次 error.code 作为 errorType;
21
+ * error.message 作为 errorMessage。
22
+ * JSON.parse 失败返回 null。
23
+ */
24
+ export declare function extractErrorInfo(body: string): {
25
+ errorType: string | null;
26
+ errorMessage: string | null;
27
+ };
28
+ /** 清理过期记录,返回删除行数 */
29
+ export declare function cleanUpstreamErrorLogs(db: Database.Database, beforeDate: string): number;
@@ -0,0 +1,40 @@
1
+ import { randomUUID } from "crypto";
2
+ /** 写入单条上游错误记录 */
3
+ export function logUpstreamError(db, entry) {
4
+ db.prepare(`
5
+ INSERT INTO upstream_error_logs
6
+ (id, request_log_id, provider_id, backend_model, status_code,
7
+ error_type, error_message, client_agent_type, router_key_id,
8
+ session_id, retry_count, created_at)
9
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
10
+ `).run(randomUUID(), entry.request_log_id, entry.provider_id, entry.backend_model, entry.status_code, entry.error_type, entry.error_message, entry.client_agent_type, entry.router_key_id, entry.session_id, entry.retry_count, new Date().toISOString());
11
+ }
12
+ /**
13
+ * 从上游响应体提取错误信息。
14
+ * 优先 error.type,其次 error.code 作为 errorType;
15
+ * error.message 作为 errorMessage。
16
+ * JSON.parse 失败返回 null。
17
+ */
18
+ export function extractErrorInfo(body) {
19
+ try {
20
+ const parsed = JSON.parse(body);
21
+ const error = parsed.error;
22
+ if (error && typeof error === "object") {
23
+ const errorType = typeof error.type === "string"
24
+ ? error.type
25
+ : typeof error.code === "string"
26
+ ? error.code
27
+ : null;
28
+ const errorMessage = typeof error.message === "string" ? error.message : null;
29
+ return { errorType, errorMessage };
30
+ }
31
+ return { errorType: null, errorMessage: null };
32
+ }
33
+ catch {
34
+ return { errorType: null, errorMessage: null };
35
+ }
36
+ }
37
+ /** 清理过期记录,返回删除行数 */
38
+ export function cleanUpstreamErrorLogs(db, beforeDate) {
39
+ return db.prepare("DELETE FROM upstream_error_logs WHERE created_at < ?").run(beforeDate).changes;
40
+ }
@@ -15,6 +15,7 @@ import { randomUUID } from "crypto";
15
15
  import { ProviderSwitchNeeded } from "../../core/errors.js";
16
16
  import { SemaphoreQueueFullError, SemaphoreTimeoutError } from "../../core/errors.js";
17
17
  import { getProviderById, updateLogClientStatus, insertRequestLog, updateLogStreamContent } from "../../db/index.js";
18
+ import { logUpstreamError, extractErrorInfo } from "../../db/upstream-error-logs.js";
18
19
  import { getSetting } from "../../db/settings.js";
19
20
  import { decrypt } from "../../utils/crypto.js";
20
21
  import { resolveMapping, filterExcluded } from "../routing/mapping-resolver.js";
@@ -374,6 +375,26 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
374
375
  const succeeded = tr.kind === "success" || tr.kind === "stream_success" || tr.kind === "stream_abort";
375
376
  if (succeeded)
376
377
  usageWindowTracker?.recordRequest(provider.id, routerKeyId ?? undefined);
378
+ // 失败时写入 upstream_error_logs
379
+ if (!succeeded) {
380
+ const body = 'body' in tr ? tr.body : '';
381
+ const { errorType, errorMessage } = extractErrorInfo(body);
382
+ const trStatusCode = getTransportStatusCode(tr);
383
+ if (trStatusCode !== null) {
384
+ logUpstreamError(db, {
385
+ request_log_id: lastLogId,
386
+ provider_id: provider.id,
387
+ backend_model: resolved.backend_model ?? clientModel,
388
+ status_code: trStatusCode,
389
+ error_type: errorType,
390
+ error_message: errorMessage,
391
+ client_agent_type: ctx.metadata.get("client_type") ?? "unknown",
392
+ router_key_id: routerKeyId,
393
+ session_id: ctx.metadata.get("session_id") ?? null,
394
+ retry_count: resilienceResult.attempts.length - 1,
395
+ });
396
+ }
397
+ }
377
398
  // 流式内容日志
378
399
  if (isStream && tracker) {
379
400
  const sc = tracker.get(logId)?.streamContent;
@@ -401,6 +422,16 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
401
422
  if (tr.kind === "success") {
402
423
  return reply.code(tr.statusCode).send(tr.body);
403
424
  }
425
+ if (tr.kind === "stream_error") {
426
+ // stream_error + headersSent 已在 orchestrator.sendResponse 中处理
427
+ // 此处为 !headersSent 分支:格式化错误体并发送
428
+ const trStatus = getTransportStatusCode(tr);
429
+ if (trStatus !== null)
430
+ updateLogClientStatus(db, lastLogId, trStatus);
431
+ const formattedBody = adapter.formatError('body' in tr ? tr.body : "stream error") ?? { error: { message: "stream error", type: "server_error" } };
432
+ reply.header("content-type", "application/json");
433
+ return reply.code(tr.statusCode).send(formattedBody);
434
+ }
404
435
  if (tr.kind === "throw" || (tr.kind === "error" && tr.statusCode >= HTTP_ERROR_THRESHOLD)) {
405
436
  const err = errors.upstreamConnectionFailed();
406
437
  updateLogClientStatus(db, lastLogId, err.statusCode);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Body matcher — 纯函数,用于结构化匹配上游响应体。
3
+ *
4
+ * 与 body_pattern(正则)互补:body_matchers 通过 JSON path + 操作符
5
+ * 做结构化匹配,避免正则的误匹配问题。
6
+ */
7
+ export interface BodyMatcher {
8
+ /** JSON 路径,如 "error.type"、"error.message",按 '.' 分割逐层访问 */
9
+ path: string;
10
+ /** 比较操作符 */
11
+ operator: "equals" | "contains" | "exists";
12
+ /** equals/contains 的期望值。exists 时忽略 */
13
+ value?: string;
14
+ }
15
+ /**
16
+ * 从嵌套对象中按点分隔路径取值。
17
+ * 中间层不存在 → undefined。不支持数组索引。
18
+ */
19
+ export declare function resolvePath(obj: unknown, path: string): unknown;
20
+ /**
21
+ * JSON body 与 matchers 匹配。所有条件 AND。
22
+ * JSON.parse 失败返回 false。
23
+ */
24
+ export declare function matchBodyMatchers(body: string, matchers: BodyMatcher[]): boolean;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Body matcher — 纯函数,用于结构化匹配上游响应体。
3
+ *
4
+ * 与 body_pattern(正则)互补:body_matchers 通过 JSON path + 操作符
5
+ * 做结构化匹配,避免正则的误匹配问题。
6
+ */
7
+ /**
8
+ * 从嵌套对象中按点分隔路径取值。
9
+ * 中间层不存在 → undefined。不支持数组索引。
10
+ */
11
+ export function resolvePath(obj, path) {
12
+ if (obj === null || obj === undefined || typeof obj !== "object") {
13
+ return undefined;
14
+ }
15
+ const segments = path.split(".");
16
+ let current = obj;
17
+ for (const segment of segments) {
18
+ if (current === null || current === undefined || typeof current !== "object") {
19
+ return undefined;
20
+ }
21
+ current = current[segment];
22
+ }
23
+ return current;
24
+ }
25
+ /**
26
+ * JSON body 与 matchers 匹配。所有条件 AND。
27
+ * JSON.parse 失败返回 false。
28
+ */
29
+ export function matchBodyMatchers(body, matchers) {
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(body);
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ for (const matcher of matchers) {
38
+ const actual = resolvePath(parsed, matcher.path);
39
+ switch (matcher.operator) {
40
+ case "exists":
41
+ if (actual === undefined)
42
+ return false;
43
+ break;
44
+ case "equals": {
45
+ if (actual === undefined)
46
+ return false;
47
+ const expected = matcher.value ?? "";
48
+ if (typeof actual === "string") {
49
+ if (actual !== expected)
50
+ return false;
51
+ }
52
+ else if (typeof actual === "number") {
53
+ if (actual.toString() !== expected)
54
+ return false;
55
+ }
56
+ else if (typeof actual === "boolean") {
57
+ if (actual.toString() !== expected)
58
+ return false;
59
+ }
60
+ else {
61
+ return false;
62
+ }
63
+ break;
64
+ }
65
+ case "contains": {
66
+ if (actual === undefined)
67
+ return false;
68
+ const expected = matcher.value ?? "";
69
+ if (typeof actual === "string") {
70
+ if (!actual.includes(expected))
71
+ return false;
72
+ }
73
+ else if (typeof actual === "number") {
74
+ if (!actual.toString().includes(expected))
75
+ return false;
76
+ }
77
+ else if (typeof actual === "boolean") {
78
+ if (!actual.toString().includes(expected))
79
+ return false;
80
+ }
81
+ else {
82
+ return false;
83
+ }
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ return true;
89
+ }
@@ -108,6 +108,7 @@ export class ProxyOrchestrator {
108
108
  failoverThreshold: ctx.failoverThreshold ?? DEFAULT_FAILOVER_THRESHOLD,
109
109
  isFailover: ctx.isFailover ?? false,
110
110
  ruleMatcher: ctx.ruleMatcher,
111
+ providerId: config.provider.id,
111
112
  };
112
113
  return this.deps.resilience.execute(() => [config.resolved], ctx.transportFn, resilienceConfig);
113
114
  }
@@ -119,7 +120,7 @@ export class ProxyOrchestrator {
119
120
  if (result.kind === "stream_error" && result.headersSent) {
120
121
  return;
121
122
  }
122
- // failover 场景下错误响应由外层 proxy-handler 控制,此处不发送
123
+ // failover 场景下错误响应由外层 failover-loop 控制,此处不发送
123
124
  if (ctx?.isFailover && "statusCode" in result && result.statusCode >= (ctx.failoverThreshold ?? DEFAULT_FAILOVER_THRESHOLD)) {
124
125
  return;
125
126
  }
@@ -27,6 +27,8 @@ export interface ResilienceConfig {
27
27
  isFailover: boolean;
28
28
  /** 全局迭代上限,防止极端配置导致 while(true) 循环过多 */
29
29
  iterationCap?: number;
30
+ /** 当前 provider ID,用于 RetryRuleMatcher 按 provider 过滤规则 */
31
+ providerId?: string;
30
32
  }
31
33
  export interface ResilienceResult {
32
34
  result: TransportResult;
@@ -74,7 +74,7 @@ export class ResilienceLayer {
74
74
  }
75
75
  const body = extractBody(result);
76
76
  if (body && config.ruleMatcher) {
77
- const matchedRule = config.ruleMatcher.match(result.statusCode, body);
77
+ const matchedRule = config.ruleMatcher.match(result.statusCode, body, config.providerId);
78
78
  if (matchedRule && state.attemptCount < matchedRule.max_retries) {
79
79
  const strategy = createStrategy(matchedRule);
80
80
  return { action: "retry", delayMs: strategy.getDelay(state.attemptCount) };
@@ -111,7 +111,7 @@ export class ResilienceLayer {
111
111
  if (result.statusCode >= config.failoverThreshold) {
112
112
  const body = extractBody(result);
113
113
  const matchedRule = body && config.ruleMatcher
114
- ? config.ruleMatcher.match(result.statusCode, body)
114
+ ? config.ruleMatcher.match(result.statusCode, body, config.providerId)
115
115
  : null;
116
116
  if (matchedRule && state.attemptCount < matchedRule.max_retries) {
117
117
  const strategy = createStrategy(matchedRule);
@@ -128,7 +128,7 @@ export class ResilienceLayer {
128
128
  // 其他响应(< failoverThreshold 的非成功) -> 仅当 rule 匹配才 retry
129
129
  const body = extractBody(result);
130
130
  if (body && config.ruleMatcher) {
131
- const matchedRule = config.ruleMatcher.match(result.statusCode, body);
131
+ const matchedRule = config.ruleMatcher.match(result.statusCode, body, config.providerId);
132
132
  if (matchedRule && state.attemptCount < matchedRule.max_retries) {
133
133
  const strategy = createStrategy(matchedRule);
134
134
  return { action: "retry", delayMs: strategy.getDelay(state.attemptCount) };
@@ -1,9 +1,11 @@
1
1
  import Database from "better-sqlite3";
2
2
  import { type RetryRule } from "../../db/retry-rules.js";
3
3
  export declare class RetryRuleMatcher {
4
+ /** Key: `${providerId ?? '__global__'}:${statusCode}` */
4
5
  private cache;
5
6
  private raw;
6
7
  load(db: Database.Database): void;
7
- match(statusCode: number, body: string): RetryRule | null;
8
- test(statusCode: number, body: string): boolean;
8
+ match(statusCode: number, body: string, providerId?: string): RetryRule | null;
9
+ test(statusCode: number, body: string, providerId?: string): boolean;
10
+ private findMatch;
9
11
  }
@@ -1,27 +1,66 @@
1
1
  import { getActiveRetryRules } from "../../db/retry-rules.js";
2
+ import { matchBodyMatchers } from "./body-matcher.js";
2
3
  export class RetryRuleMatcher {
4
+ /** Key: `${providerId ?? '__global__'}:${statusCode}` */
3
5
  cache = new Map();
4
6
  raw = [];
5
7
  load(db) {
6
8
  this.raw = getActiveRetryRules(db);
7
9
  this.cache.clear();
8
10
  for (const rule of this.raw) {
9
- const entries = this.cache.get(rule.status_code) ?? [];
10
- entries.push({ rule, pattern: new RegExp(rule.body_pattern) });
11
- this.cache.set(rule.status_code, entries);
11
+ // 解析 body_matchers JSON BodyMatcher[]
12
+ let matchers = null;
13
+ if (rule.body_matchers) {
14
+ try {
15
+ matchers = JSON.parse(rule.body_matchers);
16
+ }
17
+ catch {
18
+ matchers = null;
19
+ }
20
+ }
21
+ // 编译 body_pattern 为 RegExp(空字符串则跳过)
22
+ let pattern = null;
23
+ if (rule.body_pattern) {
24
+ pattern = new RegExp(rule.body_pattern);
25
+ }
26
+ const key = `${rule.provider_id ?? "__global__"}:${rule.status_code}`;
27
+ const entries = this.cache.get(key) ?? [];
28
+ entries.push({ rule, matchers, pattern });
29
+ this.cache.set(key, entries);
12
30
  }
13
31
  }
14
- match(statusCode, body) {
15
- const entries = this.cache.get(statusCode);
16
- if (!entries)
17
- return null;
18
- for (const { rule, pattern } of entries) {
19
- if (pattern.test(body))
20
- return rule;
32
+ match(statusCode, body, providerId) {
33
+ // 1. provider 绑定规则(优先)
34
+ if (providerId) {
35
+ const bound = this.cache.get(`${providerId}:${statusCode}`);
36
+ if (bound) {
37
+ const found = this.findMatch(bound, body);
38
+ if (found)
39
+ return found;
40
+ }
41
+ }
42
+ // 2. 查通用规则
43
+ const global = this.cache.get(`__global__:${statusCode}`);
44
+ if (global) {
45
+ return this.findMatch(global, body);
21
46
  }
22
47
  return null;
23
48
  }
24
- test(statusCode, body) {
25
- return this.match(statusCode, body) !== null;
49
+ test(statusCode, body, providerId) {
50
+ return this.match(statusCode, body, providerId) !== null;
51
+ }
52
+ findMatch(entries, body) {
53
+ for (const entry of entries) {
54
+ // 优先用结构化 matchers(如果存在),否则 fallback 到正则 pattern
55
+ if (entry.matchers !== null) {
56
+ if (matchBodyMatchers(body, entry.matchers))
57
+ return entry.rule;
58
+ }
59
+ else if (entry.pattern) {
60
+ if (entry.pattern.test(body))
61
+ return entry.rule;
62
+ }
63
+ }
64
+ return null;
26
65
  }
27
66
  }
@@ -65,7 +65,7 @@ export function buildTransportFn(p) {
65
65
  p.request.log.warn({ err, logId: p.logId }, "formatTransform stream error");
66
66
  });
67
67
  }
68
- const checkEarlyError = p.matcher ? (data) => p.matcher.test(UPSTREAM_SUCCESS, data) : undefined;
68
+ const checkEarlyError = p.matcher ? (data) => p.matcher.test(UPSTREAM_SUCCESS, data, p.provider.id) : undefined;
69
69
  const streamResult = await callStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildHeaders, metricsTransform, checkEarlyError, undefined, streamLoopGuard, p.formatTransform, p.timeoutContext, undefined, agent);
70
70
  const m = (streamResult.kind === "stream_success" || streamResult.kind === "stream_abort")
71
71
  ? streamResult.metrics : undefined;
@@ -1 +1 @@
1
- import{Gt as e,et as t,gt as n,it as r,qt as i,r as a,yt as o}from"./button-DyBccemD.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)=>(n(),t(`div`,{"data-slot":`card`,"data-size":r.size,class:i(e(a)(`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)=>(n(),t(`div`,{"data-slot":`card-content`,class:i(e(a)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[o(r.$slots,`default`)],2))}});export{c as n,l as t};
1
+ import{Gt as e,et as t,gt as n,it as r,qt as i,r as a,yt as o}from"./button-Dwb0WM4k.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)=>(n(),t(`div`,{"data-slot":`card`,"data-size":r.size,class:i(e(a)(`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)=>(n(),t(`div`,{"data-slot":`card-content`,class:i(e(a)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[o(r.$slots,`default`)],2))}});export{c as n,l as t};
@@ -1 +1 @@
1
- import{Gt as e,et as t,gt as n,it as r,qt as i,r as a,yt as o}from"./button-DyBccemD.js";var s=r({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(n(),t(`div`,{"data-slot":`card-header`,class:i(e(a)(`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)=>(n(),t(`div`,{"data-slot":`card-title`,class:i(e(a)(`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};
1
+ import{Gt as e,et as t,gt as n,it as r,qt as i,r as a,yt as o}from"./button-Dwb0WM4k.js";var s=r({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(n(),t(`div`,{"data-slot":`card-header`,class:i(e(a)(`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)=>(n(),t(`div`,{"data-slot":`card-title`,class:i(e(a)(`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{$ as e,Gt as t,H as n,J as r,L as i,Lt as a,Q as o,X as s,Xt as c,Z as l,et as u,gt as d,it as f,kt as p,qt as m,rt as h,vt as g}from"./button-Dwb0WM4k.js";import{b as _,ft as v,ht as y,v as b,y as x}from"./index-CI2C4oZ_.js";var S=i(`chevron-right`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),C=[`onMouseenter`],w={class:`truncate max-w-40`},T={key:0,class:`ml-1 text-[10px] px-1 py-px rounded bg-emerald-500/15 text-emerald-400 shrink-0`},E=[`onMouseenter`],D=[`onClick`],O={class:`truncate`},k={key:0,class:`shrink-0 text-xs text-muted-foreground`},A={key:0,class:`px-2 py-1.5 text-sm text-muted-foreground`},j=f({__name:`CascadingSelect`,props:{groups:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(i,{emit:f}){let{t:y}=n(),j=i,M=s(()=>j.placeholder||y(`common.selectPlaceholder`)),N=f,P=a(!1),F=a(null),I=s(()=>{if(!j.modelValue)return``;let e=j.groups.find(e=>e.key===j.modelValue.groupKey);if(!e)return``;let t=e.options.find(e=>e.value===j.modelValue.value);return t?`${e.label} / ${t.label}`:``});function L(e,t){N(`update:modelValue`,{groupKey:e,value:t}),P.value=!1}function R(e){P.value=e,e||(F.value=null)}return(n,a)=>(d(),o(t(_),{open:P.value,"onUpdate:open":R},{default:p(()=>[h(t(b),{"as-child":``},{default:p(()=>[l(`div`,{class:m([`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`,[i.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":P.value}]])},[l(`span`,{class:m([`truncate`,i.modelValue?`text-foreground`:`text-muted-foreground`])},c(I.value||M.value),3),h(t(v),{class:`h-4 w-4 shrink-0 opacity-50`})],2)]),_:1}),h(t(x),{align:`start`,"side-offset":4,class:`z-[200] w-auto min-w-56 overflow-visible p-1`},{default:p(()=>[(d(!0),u(r,null,g(i.groups,n=>(d(),u(`div`,{key:n.key,class:m([`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":F.value===n.key}]),onMouseenter:e=>F.value=n.key},[l(`span`,w,c(n.label),1),n.badge?(d(),u(`span`,T,c(n.badge),1)):e(``,!0),h(t(S),{class:`ml-1 h-4 w-4 shrink-0 opacity-50`}),F.value===n.key&&n.options.length>0?(d(),u(`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=>F.value=n.key},[(d(!0),u(r,null,g(n.options,t=>(d(),u(`div`,{key:t.value,class:m([`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":i.modelValue?.groupKey===n.key&&i.modelValue?.value===t.value}]),onClick:e=>L(n.key,t.value)},[l(`span`,O,c(t.label),1),t.tag?(d(),u(`span`,k,c(t.tag),1)):e(``,!0)],10,D))),128))],40,E)):e(``,!0)],42,C))),128)),i.groups.length===0?(d(),u(`div`,A,c(t(y)(`common.noOptions`)),1)):e(``,!0)]),_:1})]),_:1},8,[`open`]))}}),M=f({__name:`CascadingModelSelect`,props:{providers:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:t}){let{t:r}=n(),i=e,a=s(()=>i.placeholder||r(`mappings.selectProviderModel`)),c=t,l=s(()=>i.providers.map(e=>({key:e.provider.id,label:e.provider.name,badge:e.isNew?r(`common.new`):void 0,options:e.models.map(e=>({value:e.name,label:e.name,tag:y(e.contextWindow)}))}))),u=s(()=>i.modelValue?{groupKey:i.modelValue.provider_id,value:i.modelValue.model}:void 0);function f(e){c(`update:modelValue`,{provider_id:e.groupKey,model:e.value})}return(t,n)=>(d(),o(j,{groups:l.value,"model-value":u.value,placeholder:a.value,compact:e.compact,"onUpdate:modelValue":f},null,8,[`groups`,`model-value`,`placeholder`,`compact`]))}});export{M as t};
@@ -1 +1 @@
1
- import{$ as e,Gt as t,Jt as n,K as r,Q as i,X as a,dt as o,gt as s,i as c,it as l,kt as u,m as d,o as f,ot as p,q as m,r as h,rt as g,x as _,xt as v,yt as y}from"./button-DyBccemD.js";import{t as b}from"./VisuallyHiddenInput-CJWO0qnA.js";import{t as x}from"./RovingFocusItem-Dzeu5KRT.js";import{J as S,K as C,R as w,U as T,V as E,X as D,pt as O}from"./index-DHx0uy1i.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>D(e,t)):D(e,t)}var[A,j]=S(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=S(`CheckboxRoot`),I=l({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(n,{emit:l}){let p=n,h=l,{forwardRef:g,currentElement:_}=f(),S=A(null),w=d(p,`modelValue`,h,{defaultValue:p.defaultValue??p.falseValue,passive:p.modelValue===void 0}),E=a(()=>S?.disabled.value||p.disabled),O=a(()=>D(w.value,p.trueValue)),j=a(()=>C(S?.modelValue.value)?w.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,p.value));function P(){if(C(S?.modelValue.value))w.value===`indeterminate`?w.value=p.trueValue:w.value=O.value?p.falseValue:p.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,p.value)){let t=e.findIndex(e=>D(e,p.value));e.splice(t,1)}else e.push(p.value);S.modelValue.value=e}}let I=T(_),L=a(()=>p.id&&_.value?document.querySelector(`[for="${p.id}"]`)?.innerText:void 0);return F({disabled:E,state:j}),(n,a)=>(s(),i(v(t(S)?.rovingFocus.value?t(x):t(c)),o(n.$attrs,{id:n.id,ref:t(g),role:`checkbox`,"as-child":n.asChild,as:n.as,type:n.as===`button`?`button`:void 0,"aria-checked":t(M)(j.value)?`mixed`:j.value,"aria-required":n.required,"aria-label":n.$attrs[`aria-label`]||L.value,"data-state":t(N)(j.value),"data-disabled":E.value?``:void 0,disabled:E.value,focusable:t(S)?.rovingFocus.value?!E.value:void 0,onKeydown:r(m(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:u(()=>[y(n.$slots,`default`,{modelValue:t(w),state:j.value}),t(I)&&n.name&&!t(S)?(s(),i(t(b),{key:0,type:`checkbox`,checked:!!j.value,name:n.name,value:n.value,disabled:E.value,required:n.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):e(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=l({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(e){let{forwardRef:n}=f(),r=P();return(e,a)=>(s(),i(t(w),{present:e.forceMount||t(M)(t(r).state.value)||t(r).state.value===!0},{default:u(()=>[g(t(c),o({ref:t(n),"data-state":t(N)(t(r).state.value),"data-disabled":t(r).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:u(()=>[y(e.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=l({__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(e,{emit:r}){let a=e,c=r,l=E(_(a,`class`),c);return(e,r)=>(s(),i(t(I),o({"data-slot":`checkbox`},t(l),{class:t(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`,a.class)}),{default:u(r=>[g(t(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:u(()=>[y(e.$slots,`default`,n(p(r)),()=>[g(t(O))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
1
+ import{$ as e,Gt as t,Jt as n,K as r,Q as i,X as a,dt as o,gt as s,i as c,it as l,kt as u,m as d,o as f,ot as p,q as m,r as h,rt as g,x as _,xt as v,yt as y}from"./button-Dwb0WM4k.js";import{t as b}from"./VisuallyHiddenInput-DHeeXXfG.js";import{t as x}from"./RovingFocusItem-DntStLzY.js";import{J as S,K as C,R as w,U as T,V as E,X as D,pt as O}from"./index-CI2C4oZ_.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>D(e,t)):D(e,t)}var[A,j]=S(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=S(`CheckboxRoot`),I=l({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(n,{emit:l}){let p=n,h=l,{forwardRef:g,currentElement:_}=f(),S=A(null),w=d(p,`modelValue`,h,{defaultValue:p.defaultValue??p.falseValue,passive:p.modelValue===void 0}),E=a(()=>S?.disabled.value||p.disabled),O=a(()=>D(w.value,p.trueValue)),j=a(()=>C(S?.modelValue.value)?w.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,p.value));function P(){if(C(S?.modelValue.value))w.value===`indeterminate`?w.value=p.trueValue:w.value=O.value?p.falseValue:p.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,p.value)){let t=e.findIndex(e=>D(e,p.value));e.splice(t,1)}else e.push(p.value);S.modelValue.value=e}}let I=T(_),L=a(()=>p.id&&_.value?document.querySelector(`[for="${p.id}"]`)?.innerText:void 0);return F({disabled:E,state:j}),(n,a)=>(s(),i(v(t(S)?.rovingFocus.value?t(x):t(c)),o(n.$attrs,{id:n.id,ref:t(g),role:`checkbox`,"as-child":n.asChild,as:n.as,type:n.as===`button`?`button`:void 0,"aria-checked":t(M)(j.value)?`mixed`:j.value,"aria-required":n.required,"aria-label":n.$attrs[`aria-label`]||L.value,"data-state":t(N)(j.value),"data-disabled":E.value?``:void 0,disabled:E.value,focusable:t(S)?.rovingFocus.value?!E.value:void 0,onKeydown:r(m(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:u(()=>[y(n.$slots,`default`,{modelValue:t(w),state:j.value}),t(I)&&n.name&&!t(S)?(s(),i(t(b),{key:0,type:`checkbox`,checked:!!j.value,name:n.name,value:n.value,disabled:E.value,required:n.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):e(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=l({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(e){let{forwardRef:n}=f(),r=P();return(e,a)=>(s(),i(t(w),{present:e.forceMount||t(M)(t(r).state.value)||t(r).state.value===!0},{default:u(()=>[g(t(c),o({ref:t(n),"data-state":t(N)(t(r).state.value),"data-disabled":t(r).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:u(()=>[y(e.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=l({__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(e,{emit:r}){let a=e,c=r,l=E(_(a,`class`),c);return(e,r)=>(s(),i(t(I),o({"data-slot":`checkbox`},t(l),{class:t(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`,a.class)}),{default:u(r=>[g(t(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:u(()=>[y(e.$slots,`default`,n(p(r)),()=>[g(t(O))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
@@ -1 +1 @@
1
- import{$ as e,Et as t,Gt as n,Ht as r,Jt as i,Lt as a,Q as o,X as s,d as c,dt as l,ft as u,gt as d,i as f,it as p,kt as m,m as h,mt as g,o as _,ot as v,rt as y,yt as b}from"./button-DyBccemD.js";import{B as x,J as S,R as C,V as w}from"./index-DHx0uy1i.js";var[T,E]=S(`CollapsibleRoot`),D=p({__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(e,{expose:t,emit:i}){let a=e,s=h(a,`open`,i,{defaultValue:a.defaultOpen,passive:a.open===void 0}),{disabled:c,unmountOnHide:l}=r(a);return E({contentId:``,disabled:c,open:s,unmountOnHide:l,onOpenToggle:()=>{c.value||(s.value=!s.value)}}),t({open:s}),_(),(e,t)=>(d(),o(n(f),{as:e.as,"as-child":a.asChild,"data-state":n(s)?`open`:`closed`,"data-disabled":n(c)?``:void 0},{default:m(()=>[b(e.$slots,`default`,{open:n(s)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=p({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(r,{emit:i}){let p=r,h=i,v=T();v.contentId||=x(void 0,`reka-collapsible-content`);let S=a(),{forwardRef:w,currentElement:E}=_(),D=a(0),O=a(0),k=s(()=>v.open.value),A=a(k.value),j=a();t(()=>[k.value,S.value?.present],async()=>{await u();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=s(()=>A.value&&v.open.value);return g(()=>{requestAnimationFrame(()=>{A.value=!1})}),c(E,`beforematch`,e=>{requestAnimationFrame(()=>{v.onOpenToggle(),h(`contentFound`)})}),(t,r)=>(d(),o(n(C),{ref_key:`presentRef`,ref:S,present:t.forceMount||n(v).open.value,"force-mount":!0},{default:m(({present:r})=>[y(n(f),l(t.$attrs,{id:n(v).contentId,ref:n(w),"as-child":p.asChild,as:t.as,hidden:r?void 0:n(v).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:n(v).open.value?`open`:`closed`,"data-disabled":n(v).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:m(()=>[!n(v).unmountOnHide.value||r?b(t.$slots,`default`,{key:0}):e(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=p({__name:`Collapsible`,props:{defaultOpen:{type:Boolean},open:{type:Boolean},disabled:{type:Boolean},unmountOnHide:{type:Boolean},asChild:{type:Boolean},as:{}},emits:[`update:open`],setup(e,{emit:t}){let r=w(e,t);return(e,t)=>(d(),o(n(D),l({"data-slot":`collapsible`},n(r)),{default:m(t=>[b(e.$slots,`default`,i(v(t)))]),_:3},16))}}),A=p({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,r)=>(d(),o(n(O),l({"data-slot":`collapsible-content`},t),{default:m(()=>[b(e.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};
1
+ import{$ as e,Et as t,Gt as n,Ht as r,Jt as i,Lt as a,Q as o,X as s,d as c,dt as l,ft as u,gt as d,i as f,it as p,kt as m,m as h,mt as g,o as _,ot as v,rt as y,yt as b}from"./button-Dwb0WM4k.js";import{B as x,J as S,R as C,V as w}from"./index-CI2C4oZ_.js";var[T,E]=S(`CollapsibleRoot`),D=p({__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(e,{expose:t,emit:i}){let a=e,s=h(a,`open`,i,{defaultValue:a.defaultOpen,passive:a.open===void 0}),{disabled:c,unmountOnHide:l}=r(a);return E({contentId:``,disabled:c,open:s,unmountOnHide:l,onOpenToggle:()=>{c.value||(s.value=!s.value)}}),t({open:s}),_(),(e,t)=>(d(),o(n(f),{as:e.as,"as-child":a.asChild,"data-state":n(s)?`open`:`closed`,"data-disabled":n(c)?``:void 0},{default:m(()=>[b(e.$slots,`default`,{open:n(s)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=p({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(r,{emit:i}){let p=r,h=i,v=T();v.contentId||=x(void 0,`reka-collapsible-content`);let S=a(),{forwardRef:w,currentElement:E}=_(),D=a(0),O=a(0),k=s(()=>v.open.value),A=a(k.value),j=a();t(()=>[k.value,S.value?.present],async()=>{await u();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=s(()=>A.value&&v.open.value);return g(()=>{requestAnimationFrame(()=>{A.value=!1})}),c(E,`beforematch`,e=>{requestAnimationFrame(()=>{v.onOpenToggle(),h(`contentFound`)})}),(t,r)=>(d(),o(n(C),{ref_key:`presentRef`,ref:S,present:t.forceMount||n(v).open.value,"force-mount":!0},{default:m(({present:r})=>[y(n(f),l(t.$attrs,{id:n(v).contentId,ref:n(w),"as-child":p.asChild,as:t.as,hidden:r?void 0:n(v).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:n(v).open.value?`open`:`closed`,"data-disabled":n(v).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:m(()=>[!n(v).unmountOnHide.value||r?b(t.$slots,`default`,{key:0}):e(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=p({__name:`Collapsible`,props:{defaultOpen:{type:Boolean},open:{type:Boolean},disabled:{type:Boolean},unmountOnHide:{type:Boolean},asChild:{type:Boolean},as:{}},emits:[`update:open`],setup(e,{emit:t}){let r=w(e,t);return(e,t)=>(d(),o(n(D),l({"data-slot":`collapsible`},n(r)),{default:m(t=>[b(e.$slots,`default`,i(v(t)))]),_:3},16))}}),A=p({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,r)=>(d(),o(n(O),l({"data-slot":`collapsible-content`},t),{default:m(()=>[b(e.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};