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.
- package/dist/admin/retry-rules.js +51 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +1 -0
- package/dist/db/migrations/049_add_provider_isolation_and_matchers.sql +22 -0
- package/dist/db/retry-rules.d.ts +5 -1
- package/dist/db/retry-rules.js +3 -3
- package/dist/db/upstream-error-logs.d.ts +29 -0
- package/dist/db/upstream-error-logs.js +40 -0
- package/dist/proxy/handler/failover-loop.js +31 -0
- package/dist/proxy/orchestration/body-matcher.d.ts +24 -0
- package/dist/proxy/orchestration/body-matcher.js +89 -0
- package/dist/proxy/orchestration/orchestrator.js +2 -1
- package/dist/proxy/orchestration/resilience.d.ts +2 -0
- package/dist/proxy/orchestration/resilience.js +3 -3
- package/dist/proxy/orchestration/retry-rules.d.ts +4 -2
- package/dist/proxy/orchestration/retry-rules.js +51 -12
- package/dist/proxy/transport/transport-fn.js +1 -1
- package/frontend-dist/assets/{CardContent-DCQR2368.js → CardContent-DqY4C5hv.js} +1 -1
- package/frontend-dist/assets/{CardTitle-BhMM67Wj.js → CardTitle-CIW3zaxJ.js} +1 -1
- package/frontend-dist/assets/CascadingModelSelect-Bt4Pp5CS.js +1 -0
- package/frontend-dist/assets/{Checkbox-B5YofFYK.js → Checkbox-Dtzyj_Mx.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-Bn1QXK6j.js → CollapsibleContent-DrIK1N9f.js} +1 -1
- package/frontend-dist/assets/CollapsibleTrigger-0C6Isfde.js +1 -0
- package/frontend-dist/assets/{Dashboard-CAMubYhi.js → Dashboard-iz-xysfd.js} +2 -2
- package/frontend-dist/assets/{Input-D1Dgx5aQ.js → Input-CvlJOI6Z.js} +1 -1
- package/frontend-dist/assets/{Label-BmVN9gSu.js → Label-CwsQ60Yf.js} +1 -1
- package/frontend-dist/assets/{Login-BrGvgfxl.js → Login-DSjsqE-c.js} +1 -1
- package/frontend-dist/assets/Logs-9rFY2MsD.js +1 -0
- package/frontend-dist/assets/MappingEntryEditor-DPOCfF3z.js +1 -0
- package/frontend-dist/assets/ModelMappings-B3FeIPnC.js +1 -0
- package/frontend-dist/assets/Monitor-IuX9QMJH.js +1 -0
- package/frontend-dist/assets/{Providers-MrtjZEAT.js → Providers-CGKROJ83.js} +1 -1
- package/frontend-dist/assets/ProxyEnhancement-Sc3CQpkn.js +1 -0
- package/frontend-dist/assets/{QuickSetup-BRvO-y_3.js → QuickSetup-C2kDKJ7l.js} +1 -1
- package/frontend-dist/assets/RetryRules-ekDQR17j.js +1 -0
- package/frontend-dist/assets/RouterKeys-BYDmosW1.js +1 -0
- package/frontend-dist/assets/{RovingFocusItem-Dzeu5KRT.js → RovingFocusItem-DntStLzY.js} +1 -1
- package/frontend-dist/assets/Schedules-Sdzde0R2.js +1 -0
- package/frontend-dist/assets/{Settings-BuV0EjD9.js → Settings-CvOesX4a.js} +1 -1
- package/frontend-dist/assets/{Setup-nOtjStYO.js → Setup-DhLn54zX.js} +1 -1
- package/frontend-dist/assets/{Switch-CwKiYHl-.js → Switch-D8VfSEnf.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-CGUpAsNa.js → TooltipTrigger-BKICqqnF.js} +1 -1
- package/frontend-dist/assets/{TransformRulesForm-Bk4hpVsX.js → TransformRulesForm-Ch_KBFUw.js} +1 -1
- package/frontend-dist/assets/UnifiedRequestDialog-Bst4BwB4.js +3 -0
- package/frontend-dist/assets/{VisuallyHiddenInput-CJWO0qnA.js → VisuallyHiddenInput-DHeeXXfG.js} +1 -1
- package/frontend-dist/assets/{button-DyBccemD.js → button-Dwb0WM4k.js} +2 -2
- package/frontend-dist/assets/{copy-pLU8Wg9s.js → copy-z2wnatoS.js} +1 -1
- package/frontend-dist/assets/{dialog-DTyTrRWK.js → dialog-7EH9lnXi.js} +1 -1
- package/frontend-dist/assets/index-BaNw4aag.css +1 -0
- package/frontend-dist/assets/{index-DHx0uy1i.js → index-CI2C4oZ_.js} +2 -2
- package/frontend-dist/assets/logs-DAfOmnUs.js +1 -0
- package/frontend-dist/assets/logs-DkiQi5ER.js +1 -0
- package/frontend-dist/assets/{model-patches-CYdOvNIM.js → model-patches-z92RV8oY.js} +1 -1
- package/frontend-dist/assets/plus-CJe5xtMJ.js +1 -0
- package/frontend-dist/assets/{providers-BjaFz2uN.js → providers-ChuD67aW.js} +1 -1
- package/frontend-dist/assets/{providers-Djvbh2Pk.js → providers-DEOmviin.js} +1 -1
- package/frontend-dist/assets/retryRules-CEGYoM2X.js +1 -0
- package/frontend-dist/assets/retryRules-DGg26acb.js +3 -0
- package/frontend-dist/assets/routerKeys-C4oCPJrT.js +1 -0
- package/frontend-dist/assets/routerKeys-CyZ4L-1h.js +1 -0
- package/frontend-dist/assets/{sparkles-BUUu02Fq.js → sparkles-4KPHhHvW.js} +1 -1
- package/frontend-dist/assets/{trash-2-2tgBiASz.js → trash-2-l1HwaMsK.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-DnRHF6ie.js → useLogRetention-BeiWQy2K.js} +1 -1
- package/frontend-dist/index.html +3 -3
- package/package.json +1 -1
- package/frontend-dist/assets/CascadingModelSelect-CTodFvdd.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-BBkiWl_d.js +0 -1
- package/frontend-dist/assets/Logs-PrF4ijTc.js +0 -1
- package/frontend-dist/assets/MappingEntryEditor-C3r7weks.js +0 -1
- package/frontend-dist/assets/ModelMappings-DfuSP8wW.js +0 -1
- package/frontend-dist/assets/Monitor-Db_yxvjc.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-sFtoczuy.js +0 -1
- package/frontend-dist/assets/RetryRules-D722cpmL.js +0 -1
- package/frontend-dist/assets/RouterKeys-sY5Y77qI.js +0 -1
- package/frontend-dist/assets/Schedules-De0ouYPh.js +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-DPocBUjz.js +0 -3
- package/frontend-dist/assets/index-BowCJXHo.css +0 -1
- package/frontend-dist/assets/logs-C8j2wv9U.js +0 -1
- package/frontend-dist/assets/logs-DXEeXyQL.js +0 -1
- package/frontend-dist/assets/retryRules-Btt-s8hs.js +0 -1
- package/frontend-dist/assets/retryRules-Cnh9jDD4.js +0 -1
- package/frontend-dist/assets/routerKeys-BKIv9voD.js +0 -1
- package/frontend-dist/assets/routerKeys-DHqew7e3.js +0 -1
- package/frontend-dist/assets/useClipboard-BnExJY_c.js +0 -1
- /package/frontend-dist/assets/{common-DpEjrxgC.js → common-BCe8fK5N.js} +0 -0
- /package/frontend-dist/assets/{common-Cg4OGISS.js → common-CdzdKRsV.js} +0 -0
- /package/frontend-dist/assets/{dashboard-oYrGiYFH.js → dashboard-C0XEgNnb.js} +0 -0
- /package/frontend-dist/assets/{dashboard-DxQj2qDW.js → dashboard-uBv6TkGn.js} +0 -0
- /package/frontend-dist/assets/{login-Cqit6dLn.js → login-BozjQrft.js} +0 -0
- /package/frontend-dist/assets/{login-COgZiZU0.js → login-DXMVxesL.js} +0 -0
- /package/frontend-dist/assets/{mappings-CIi5L6vx.js → mappings-B6NCfNio.js} +0 -0
- /package/frontend-dist/assets/{mappings-DK14Q480.js → mappings-yo-RxHK6.js} +0 -0
- /package/frontend-dist/assets/{monitor-BrKGZyOA.js → monitor-CcfwHbJF.js} +0 -0
- /package/frontend-dist/assets/{monitor-sNuyagci.js → monitor-XNX6LZGC.js} +0 -0
- /package/frontend-dist/assets/{proxyEnhancement-DsQ6_BKy.js → proxyEnhancement-BC3eVKDp.js} +0 -0
- /package/frontend-dist/assets/{proxyEnhancement-Caq4cKe6.js → proxyEnhancement-Dq47MAAI.js} +0 -0
- /package/frontend-dist/assets/{quickSetup-BL0txMvb.js → quickSetup-C8jt6VzA.js} +0 -0
- /package/frontend-dist/assets/{quickSetup-CvR1GTCW.js → quickSetup-tMWsXvo3.js} +0 -0
- /package/frontend-dist/assets/{requestDetail-DDzGbK-Q.js → requestDetail-BI1tm09T.js} +0 -0
- /package/frontend-dist/assets/{requestDetail-C6o1ku8x.js → requestDetail-dJ1P4rgQ.js} +0 -0
- /package/frontend-dist/assets/{schedules-Chof0Byr.js → schedules-ByzpVhl2.js} +0 -0
- /package/frontend-dist/assets/{schedules-BdCs4P0W.js → schedules-tSjne6S-.js} +0 -0
- /package/frontend-dist/assets/{settings-DheKiB0E.js → settings-CveTb9z9.js} +0 -0
- /package/frontend-dist/assets/{settings-BAfdizNX.js → settings-DQqli04G.js} +0 -0
- /package/frontend-dist/assets/{setup-AGblmz9n.js → setup-BKw9vkV3.js} +0 -0
- /package/frontend-dist/assets/{setup-CghpqjMU.js → setup-D9yk-VSM.js} +0 -0
- /package/frontend-dist/assets/{sidebar-CpYOxTtl.js → sidebar-HuUg5Wsk.js} +0 -0
- /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 });
|
package/dist/db/index.d.ts
CHANGED
|
@@ -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);
|
package/dist/db/retry-rules.d.ts
CHANGED
|
@@ -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;
|
package/dist/db/retry-rules.js
CHANGED
|
@@ -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 场景下错误响应由外层
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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};
|
package/frontend-dist/assets/{CollapsibleContent-Bn1QXK6j.js → CollapsibleContent-DrIK1N9f.js}
RENAMED
|
@@ -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-
|
|
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};
|