llm-simple-router 0.11.2 → 0.11.5
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/proxy-enhancement.js +25 -5
- package/dist/admin/retry-rules.js +318 -1
- package/dist/proxy/proxy-logging.js +30 -1
- package/dist/proxy/transform/request-bridge-responses.js +4 -1
- package/dist/proxy/transform/request-transform-responses.js +4 -1
- package/dist/proxy/transform/shared-normalize.d.ts +7 -0
- package/dist/proxy/transform/shared-normalize.js +14 -0
- package/dist/utils/llm-client.d.ts +17 -0
- package/dist/utils/llm-client.js +78 -0
- package/frontend-dist/assets/{CardContent-D1CMiLP7.js → CardContent-BpezvZs3.js} +1 -1
- package/frontend-dist/assets/{CardTitle-BqQP3-E2.js → CardTitle-DGE-0Oy-.js} +1 -1
- package/frontend-dist/assets/CascadingModelSelect-Cm8SOfvy.js +1 -0
- package/frontend-dist/assets/{Checkbox-DrDlKj-G.js → Checkbox-WeiIpTOy.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-uU516NPV.js → CollapsibleContent-CmDbVrkh.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-BlnmToB3.js → CollapsibleTrigger-BXzK-Ixj.js} +1 -1
- package/frontend-dist/assets/{Dashboard-DIh5ljvA.js → Dashboard-BR2CJ3cj.js} +2 -2
- package/frontend-dist/assets/{Input-CdfjEjbl.js → Input-oTBwkQba.js} +1 -1
- package/frontend-dist/assets/{Label-8BU9m_xR.js → Label-Bd9CgUhC.js} +1 -1
- package/frontend-dist/assets/{Login-RzXg2ypt.js → Login-DUFnfFIU.js} +1 -1
- package/frontend-dist/assets/Logs-rClEHKtY.js +1 -0
- package/frontend-dist/assets/MappingEntryEditor-BbhYb7UZ.js +1 -0
- package/frontend-dist/assets/ModelMappings-CHfRgKOc.js +1 -0
- package/frontend-dist/assets/{Monitor-6qk9XIoS.js → Monitor-BxX3NJ6B.js} +1 -1
- package/frontend-dist/assets/{Providers-BfXjRs4P.js → Providers-DtpzIBBK.js} +1 -1
- package/frontend-dist/assets/ProxyEnhancement-CwXkuoUt.js +1 -0
- package/frontend-dist/assets/QuickSetup-CyVSAFVv.js +1 -0
- package/frontend-dist/assets/{RetryRules-XoxQfjvu.js → RetryRules-DumH9Gsl.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-C6IJ-JiQ.js → RouterKeys-WQw9THbd.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-Brs0DuXq.js → RovingFocusItem-BRjpvwsE.js} +1 -1
- package/frontend-dist/assets/Schedules-C7Kuve1s.js +1 -0
- package/frontend-dist/assets/{Settings-hNwSgyS8.js → Settings-D4Ybryvp.js} +1 -1
- package/frontend-dist/assets/{Setup-C9Mwsbh-.js → Setup-jcy-ADqe.js} +1 -1
- package/frontend-dist/assets/Switch-Djefuot9.js +1 -0
- package/frontend-dist/assets/{TooltipTrigger-DhF7HDYV.js → TooltipTrigger-90_Rc8W6.js} +1 -1
- package/frontend-dist/assets/{TransformRulesForm-CzMZxJ5E.js → TransformRulesForm-Ds37qMrv.js} +1 -1
- package/frontend-dist/assets/UnifiedRequestDialog-Bj4AG0sQ.css +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-cXmSFFit.js +3 -0
- package/frontend-dist/assets/{VisuallyHiddenInput-DQTfxmA1.js → VisuallyHiddenInput-CRvhHFE3.js} +1 -1
- package/frontend-dist/assets/{button-lvbllMnE.js → button-0YdRDsHo.js} +2 -2
- package/frontend-dist/assets/{copy-CA0xNeW8.js → copy-Kb93f3pX.js} +1 -1
- package/frontend-dist/assets/{dialog-D3UelwTP.js → dialog-CJYBiMC2.js} +1 -1
- package/frontend-dist/assets/index-BowCJXHo.css +1 -0
- package/frontend-dist/assets/{index-NiDVXv3S.js → index-Cpt-6ePL.js} +2 -2
- package/frontend-dist/assets/logs-C8j2wv9U.js +1 -0
- package/frontend-dist/assets/logs-DXEeXyQL.js +1 -0
- package/frontend-dist/assets/{model-patches-Ca_KuITM.js → model-patches-D5JUBh-5.js} +1 -1
- package/frontend-dist/assets/proxyEnhancement-Caq4cKe6.js +3 -0
- package/frontend-dist/assets/proxyEnhancement-DsQ6_BKy.js +3 -0
- package/frontend-dist/assets/{retryRules-CzLnagW_.js → retryRules-Btt-s8hs.js} +1 -1
- package/frontend-dist/assets/{retryRules-C--dd-y8.js → retryRules-Cnh9jDD4.js} +1 -1
- package/frontend-dist/assets/sparkles-BUhc67Q2.js +1 -0
- package/frontend-dist/assets/{trash-2-Vyzwv3La.js → trash-2-EvdcnMxN.js} +1 -1
- package/frontend-dist/assets/{useClipboard-Dy0rnj9d.js → useClipboard-C7DExKK3.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-CaCCaDeG.js → useLogRetention-DdntmZh0.js} +1 -1
- package/frontend-dist/index.html +3 -3
- package/package.json +2 -2
- package/frontend-dist/assets/Logs-CIpseYy0.js +0 -1
- package/frontend-dist/assets/MappingEntryEditor-BjeYox3a.js +0 -1
- package/frontend-dist/assets/ModelMappings-BcSQmvr-.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-BDLb_u6a.js +0 -1
- package/frontend-dist/assets/QuickSetup-B3S4Favg.js +0 -1
- package/frontend-dist/assets/Schedules-Xe8X0SdY.js +0 -1
- package/frontend-dist/assets/Switch-Ctea7ISh.js +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-C4MTxb25.css +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-Dt9EGhxH.js +0 -3
- package/frontend-dist/assets/index-CogX4xoq.css +0 -1
- package/frontend-dist/assets/logs-CA8USnXG.js +0 -1
- package/frontend-dist/assets/logs-QPt2Ybwy.js +0 -1
- package/frontend-dist/assets/proxyEnhancement-B6vdsMeK.js +0 -3
- package/frontend-dist/assets/proxyEnhancement-UuPFs4M3.js +0 -3
- /package/frontend-dist/assets/{common-Cn0QcrnY.js → common-Cg4OGISS.js} +0 -0
- /package/frontend-dist/assets/{common-Bvxev9Ev.js → common-DpEjrxgC.js} +0 -0
- /package/frontend-dist/assets/{dashboard-BpAReE3I.js → dashboard-DxQj2qDW.js} +0 -0
- /package/frontend-dist/assets/{dashboard-K_VT51kT.js → dashboard-oYrGiYFH.js} +0 -0
- /package/frontend-dist/assets/{login-BkOvA7gg.js → login-COgZiZU0.js} +0 -0
- /package/frontend-dist/assets/{login-DWRFsEu3.js → login-Cqit6dLn.js} +0 -0
- /package/frontend-dist/assets/{mappings-BpkOqnsu.js → mappings-CIi5L6vx.js} +0 -0
- /package/frontend-dist/assets/{mappings-D7Qy46v_.js → mappings-DK14Q480.js} +0 -0
- /package/frontend-dist/assets/{monitor-CcPZdXUM.js → monitor-BrKGZyOA.js} +0 -0
- /package/frontend-dist/assets/{monitor-D-0KOVTC.js → monitor-sNuyagci.js} +0 -0
- /package/frontend-dist/assets/{providers-BI5dO-j0.js → providers-BjaFz2uN.js} +0 -0
- /package/frontend-dist/assets/{providers-BzxbZ85B.js → providers-Djvbh2Pk.js} +0 -0
- /package/frontend-dist/assets/{quickSetup-sDxsfeH3.js → quickSetup-BL0txMvb.js} +0 -0
- /package/frontend-dist/assets/{quickSetup-9__wUdpr.js → quickSetup-CvR1GTCW.js} +0 -0
- /package/frontend-dist/assets/{requestDetail-8Sp9tWNb.js → requestDetail-C6o1ku8x.js} +0 -0
- /package/frontend-dist/assets/{requestDetail-CcHzzKYr.js → requestDetail-DDzGbK-Q.js} +0 -0
- /package/frontend-dist/assets/{routerKeys-CB2l_V7w.js → routerKeys-BKIv9voD.js} +0 -0
- /package/frontend-dist/assets/{routerKeys-p_ioAckE.js → routerKeys-DHqew7e3.js} +0 -0
- /package/frontend-dist/assets/{schedules-Cz_-Wfa_.js → schedules-BdCs4P0W.js} +0 -0
- /package/frontend-dist/assets/{schedules-DTgk603B.js → schedules-Chof0Byr.js} +0 -0
- /package/frontend-dist/assets/{settings-B5Mq1HN8.js → settings-BAfdizNX.js} +0 -0
- /package/frontend-dist/assets/{settings-j3dzVXzy.js → settings-DheKiB0E.js} +0 -0
- /package/frontend-dist/assets/{setup-Dryg-9wL.js → setup-AGblmz9n.js} +0 -0
- /package/frontend-dist/assets/{setup-DaeEG9ll.js → setup-CghpqjMU.js} +0 -0
- /package/frontend-dist/assets/{sidebar-BQWT-QZb.js → sidebar-BMFaYdll.js} +0 -0
- /package/frontend-dist/assets/{sidebar-DYwEKca3.js → sidebar-CpYOxTtl.js} +0 -0
|
@@ -6,6 +6,10 @@ const UpdateProxyEnhancementSchema = Type.Object({
|
|
|
6
6
|
stream_loop_enabled: Type.Boolean(),
|
|
7
7
|
tool_round_limit_enabled: Type.Boolean(),
|
|
8
8
|
tool_error_logging_enabled: Type.Boolean(),
|
|
9
|
+
ai_retry_config: Type.Optional(Type.Union([
|
|
10
|
+
Type.Null(),
|
|
11
|
+
Type.Object({ provider_id: Type.String({ minLength: 1 }), model: Type.String({ minLength: 1 }) }),
|
|
12
|
+
])),
|
|
9
13
|
});
|
|
10
14
|
export const adminProxyEnhancementRoutes = (app, options, done) => {
|
|
11
15
|
const { db } = options;
|
|
@@ -25,18 +29,34 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
|
|
|
25
29
|
}
|
|
26
30
|
catch { /* eslint-disable-line taste/no-silent-catch -- invalid JSON, return defaults */ }
|
|
27
31
|
}
|
|
28
|
-
|
|
32
|
+
const aiConfigRaw = getSetting(db, "ai_retry_config");
|
|
33
|
+
let aiRetryConfig = null;
|
|
34
|
+
if (aiConfigRaw) {
|
|
35
|
+
try {
|
|
36
|
+
aiRetryConfig = JSON.parse(aiConfigRaw);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
console.error('proxyEnhancement.parseAiConfig:', e);
|
|
40
|
+
aiRetryConfig = null; // 损坏的 JSON 回退为 null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return reply.send({ ...config, ai_retry_config: aiRetryConfig });
|
|
29
44
|
});
|
|
30
45
|
app.put("/admin/api/proxy-enhancement", { schema: { body: UpdateProxyEnhancementSchema } }, async (request, reply) => {
|
|
31
46
|
const body = request.body;
|
|
47
|
+
const { ai_retry_config, ...enhancementFields } = body;
|
|
32
48
|
const config = {
|
|
33
|
-
tool_call_loop_enabled:
|
|
34
|
-
stream_loop_enabled:
|
|
35
|
-
tool_round_limit_enabled:
|
|
36
|
-
tool_error_logging_enabled:
|
|
49
|
+
tool_call_loop_enabled: enhancementFields.tool_call_loop_enabled,
|
|
50
|
+
stream_loop_enabled: enhancementFields.stream_loop_enabled,
|
|
51
|
+
tool_round_limit_enabled: enhancementFields.tool_round_limit_enabled,
|
|
52
|
+
tool_error_logging_enabled: enhancementFields.tool_error_logging_enabled,
|
|
37
53
|
};
|
|
38
54
|
setSetting(db, "proxy_enhancement", JSON.stringify(config));
|
|
39
55
|
clearEnhancementConfigCache();
|
|
56
|
+
// ai_retry_config is stored in a separate settings key
|
|
57
|
+
if (ai_retry_config !== undefined) {
|
|
58
|
+
setSetting(db, "ai_retry_config", ai_retry_config ? JSON.stringify(ai_retry_config) : "");
|
|
59
|
+
}
|
|
40
60
|
return reply.send({ success: true });
|
|
41
61
|
});
|
|
42
62
|
done();
|
|
@@ -1,7 +1,92 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, } from "../db/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { callLLM } from "../utils/llm-client.js";
|
|
4
|
+
import { getActiveRetryRules } from "../db/retry-rules.js";
|
|
5
|
+
import { getRequestLogById } from "../db/logs.js";
|
|
6
|
+
import { getProviderById } from "../db/providers.js";
|
|
7
|
+
import { getSetting } from "../db/settings.js";
|
|
8
|
+
import { decrypt } from "../utils/crypto.js";
|
|
9
|
+
import { HTTP_OK, HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND } from "./constants.js";
|
|
4
10
|
import { API_CODE, apiError } from "./api-response.js";
|
|
11
|
+
// AI 重试规则的 system prompt 模板(内联避免运行时文件依赖)
|
|
12
|
+
const AI_RETRY_PROMPT_TEMPLATE = `You are an API retry rule expert. Your ONLY job is to analyze the error response and output a JSON retry rule.
|
|
13
|
+
|
|
14
|
+
## STEP 1: Check if the response contains an error
|
|
15
|
+
If the response is successful (no error content), output this JSON:
|
|
16
|
+
{"error":"Unable to generate rule: normal response"}
|
|
17
|
+
|
|
18
|
+
## STEP 2: Extract the error identifier from Response Body
|
|
19
|
+
Look at the Response Body JSON. Find the specific error identifier:
|
|
20
|
+
- If JSON has \`{"error":{"code":"..."}}\` → the identifier is the value of \`error.code\`
|
|
21
|
+
- If JSON has \`{"error":{"type":"..."}}\` → the identifier is the value of \`error.type\`
|
|
22
|
+
- If JSON has \`{"error":{"message":"..."}}\` → extract a short distinctive keyword from the message
|
|
23
|
+
- If none of these, look for any structured error field
|
|
24
|
+
|
|
25
|
+
## STEP 3: Build body_pattern
|
|
26
|
+
\`body_pattern\` is a regex that matches the JSON structure of this error.
|
|
27
|
+
|
|
28
|
+
Rules:
|
|
29
|
+
- MUST be a regex against JSON text, NOT plain text
|
|
30
|
+
- MUST include the JSON key path to anchor the match (e.g. \`"error".*"code"\`)
|
|
31
|
+
- MUST match the actual keys in the response. If response has \`"code"\`, use \`"code"\`. If response has \`"type"\`, use \`"type"\`. Do NOT guess keys that don't exist in the response.
|
|
32
|
+
- Use \`\\s*:\\s*\` between key and value (allows optional spaces around colon)
|
|
33
|
+
- Do NOT use \`.*\` to match everything — be specific
|
|
34
|
+
|
|
35
|
+
Correct examples (what you output in body_pattern):
|
|
36
|
+
- Response \`{"error":{"code":"1305"}}\` → \`"error".*"code"\\s*:\\s*"1305"\`
|
|
37
|
+
- Response \`{"error":{"code":"rate_limit_error"}}\` → \`"error".*"code"\\s*:\\s*"rate_limit_error"\`
|
|
38
|
+
- Response \`{"error":{"message":"请稍后重试"}}\` → \`"error".*"请稍后重试"\`
|
|
39
|
+
|
|
40
|
+
Wrong examples (DO NOT do this):
|
|
41
|
+
- \`rate_limit_error\` (missing JSON key context — matches anywhere in body)
|
|
42
|
+
- \`"type".*"rate_limit"\` (response has \`"code"\` not \`"type"\` — wrong key)
|
|
43
|
+
|
|
44
|
+
## STEP 4: Determine retry parameters
|
|
45
|
+
ALWAYS use these values, never change them:
|
|
46
|
+
- retry_strategy: "exponential"
|
|
47
|
+
- retry_delay_ms: 5000
|
|
48
|
+
- max_retries: 10
|
|
49
|
+
- max_delay_ms: 60000
|
|
50
|
+
|
|
51
|
+
## STEP 5: Build the name field
|
|
52
|
+
\`name\` is the display name shown in the UI rules list. Follow these rules EXACTLY:
|
|
53
|
+
|
|
54
|
+
1. **Provider**: Use the Provider value from the user prompt (e.g. "ZAI", "OpenCode", "DeepSeek"). This is a human-readable name. NEVER use a UUID or provider_id like "f822eb4a".
|
|
55
|
+
2. **Description**: A SHORT Chinese phrase describing the error type:
|
|
56
|
+
- 速率限制 (rate limit)
|
|
57
|
+
- 认证错误 (authentication error)
|
|
58
|
+
- 模型过载 (model overloaded)
|
|
59
|
+
- 临时不可用 (temporarily unavailable)
|
|
60
|
+
- 网络错误 (network error)
|
|
61
|
+
- SSE错误 (SSE streaming error)
|
|
62
|
+
- 操作失败 (operation failed)
|
|
63
|
+
3. **HTTP info**: Always include HTTP status code in parentheses. If you found a specific error code/type in Step 2, include it after the status code.
|
|
64
|
+
4. **Model name**: Do NOT include model name by default. Only include it if the Response Body text explicitly mentions a specific model causing the error (e.g. "model deepseek-chat is overloaded"). Rate limits, auth errors, generic server errors — these are provider-level, do NOT include model name.
|
|
65
|
+
|
|
66
|
+
Format:
|
|
67
|
+
- Without error code: \`{Provider} {描述} (HTTP {status})\`
|
|
68
|
+
- With error code: \`{Provider} {描述} (HTTP {status}, code {error_code})\`
|
|
69
|
+
- With model (only if response mentions model): \`{Provider} {model} {描述} (HTTP {status}, code {error_code})\`
|
|
70
|
+
|
|
71
|
+
Reference names from production rules:
|
|
72
|
+
- \`ZAI 速率限制 (HTTP 200, code 1302)\`
|
|
73
|
+
- \`ZAI 临时不可用 (HTTP 200)\`
|
|
74
|
+
- \`ZAI 模型过载 (HTTP 429, code 1305)\`
|
|
75
|
+
- \`KIMI 401 认证错误\`
|
|
76
|
+
- \`OpenCode DeepSeek 速率限制 (HTTP 429, type rate_limit_error)\`
|
|
77
|
+
|
|
78
|
+
## STEP 6: Build the summary field
|
|
79
|
+
\`summary\` is a one-line Chinese description. Same content as \`name\` but use Chinese full-width parentheses:
|
|
80
|
+
- Without error code: \`{Provider} {描述}(HTTP {status})\`
|
|
81
|
+
- With error code: \`{Provider} {描述}(HTTP {status},code {error_code})\`
|
|
82
|
+
|
|
83
|
+
## STEP 7: Check for duplicates
|
|
84
|
+
Compare against the Existing Rules list. If a rule already covers this exact \`status_code\` + \`body_pattern\` combination, output:
|
|
85
|
+
{"error":"Duplicate rule: similar to [existing rule name]"}
|
|
86
|
+
|
|
87
|
+
## STEP 8: Output the final JSON
|
|
88
|
+
Output ONLY this JSON object, no other text:
|
|
89
|
+
{"summary":"...","name":"...","status_code":200,"body_pattern":"...","retry_strategy":"exponential","retry_delay_ms":5000,"max_retries":10,"max_delay_ms":60000}`;
|
|
5
90
|
const DEFAULT_RETRY_DELAY_MS = 5000;
|
|
6
91
|
const DEFAULT_MAX_RETRIES = 10;
|
|
7
92
|
const DEFAULT_MAX_DELAY_MS = 60000;
|
|
@@ -34,6 +119,121 @@ function validateBodyPattern(pattern) {
|
|
|
34
119
|
return "Invalid body_pattern regex";
|
|
35
120
|
}
|
|
36
121
|
}
|
|
122
|
+
// ---------- AI Retry Rule Generation Helpers ----------
|
|
123
|
+
const MAX_RESPONSE_CHARS = 4000;
|
|
124
|
+
const STATUS_CODE_MIN = 100;
|
|
125
|
+
const STATUS_CODE_MAX = 599;
|
|
126
|
+
const MAX_RETRIES_UPPER = 100;
|
|
127
|
+
/** 从日志中提取响应文本,优先 upstream_response,回退 stream_text_content */
|
|
128
|
+
function extractResponseText(log) {
|
|
129
|
+
const raw = log.upstream_response || log.stream_text_content || "";
|
|
130
|
+
if (raw.length <= MAX_RESPONSE_CHARS)
|
|
131
|
+
return raw;
|
|
132
|
+
const TRUNCATION_SUFFIX = "\n...(truncated)";
|
|
133
|
+
const truncated = raw.substring(0, MAX_RESPONSE_CHARS - TRUNCATION_SUFFIX.length);
|
|
134
|
+
// 在 JSON 边界处截断,避免破坏键值对导致 AI 生成无效正则
|
|
135
|
+
const lastBrace = truncated.lastIndexOf("}");
|
|
136
|
+
const lastBracket = truncated.lastIndexOf("]");
|
|
137
|
+
const cutPoint = Math.max(lastBrace, lastBracket);
|
|
138
|
+
const MIN_RATIO_FOR_BOUNDARY_CUT = 0.5;
|
|
139
|
+
return cutPoint > truncated.length * MIN_RATIO_FOR_BOUNDARY_CUT ? truncated.substring(0, cutPoint + 1) + TRUNCATION_SUFFIX : truncated + TRUNCATION_SUFFIX;
|
|
140
|
+
}
|
|
141
|
+
/** 检查文本是否包含错误特征关键词(case-insensitive) */
|
|
142
|
+
function hasErrorFeatures(text) {
|
|
143
|
+
if (!text)
|
|
144
|
+
return false;
|
|
145
|
+
const lower = text.toLowerCase();
|
|
146
|
+
return lower.includes("error");
|
|
147
|
+
}
|
|
148
|
+
/** 解析 AI 返回的 JSON,支持 ```json 代码块包裹 */
|
|
149
|
+
function parseAIContent(content) {
|
|
150
|
+
const codeBlockMatch = content.match(/```json\s*([\s\S]*?)```/);
|
|
151
|
+
const jsonStr = codeBlockMatch ? codeBlockMatch[1].trim() : content.trim();
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(jsonStr);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** 从 AI 返回的 error 字段提取可读错误信息(兼容 string 和 object 两种格式) */
|
|
160
|
+
function extractErrorMessage(error) {
|
|
161
|
+
if (typeof error === "string")
|
|
162
|
+
return error;
|
|
163
|
+
const obj = error;
|
|
164
|
+
const msg = obj.message;
|
|
165
|
+
return typeof msg === "string" ? msg : JSON.stringify(error);
|
|
166
|
+
}
|
|
167
|
+
/** 校验 AI 生成的规则字段,返回错误描述或 null */
|
|
168
|
+
function validateAIRule(parsed) {
|
|
169
|
+
if (typeof parsed.summary !== "string" || parsed.summary.trim() === "") {
|
|
170
|
+
return "summary is required";
|
|
171
|
+
}
|
|
172
|
+
if (typeof parsed.name !== "string" || parsed.name.trim() === "") {
|
|
173
|
+
return "name is required";
|
|
174
|
+
}
|
|
175
|
+
if (typeof parsed.status_code !== "number" || !Number.isInteger(parsed.status_code) || parsed.status_code < STATUS_CODE_MIN || parsed.status_code > STATUS_CODE_MAX) {
|
|
176
|
+
return "status_code must be 100-599";
|
|
177
|
+
}
|
|
178
|
+
if (typeof parsed.body_pattern !== "string") {
|
|
179
|
+
return "body_pattern is required";
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
new RegExp(parsed.body_pattern);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return "body_pattern is not a valid regex";
|
|
186
|
+
}
|
|
187
|
+
// ReDoS 防护:限制正则长度 + 检测已知危险模式
|
|
188
|
+
const MAX_PATTERN_LENGTH = 500;
|
|
189
|
+
if (parsed.body_pattern.length > MAX_PATTERN_LENGTH) {
|
|
190
|
+
return `Rule validation failed: body_pattern too long (max ${MAX_PATTERN_LENGTH} chars)`;
|
|
191
|
+
}
|
|
192
|
+
const DANGEROUS_REGEX_PATTERNS = [
|
|
193
|
+
/\([^)]*\+[^)]*\+/, // 嵌套量词如 (a+b+)+
|
|
194
|
+
/\([^)]*[*+][^)]*\)\s*[*+]/, // 重复分组 + 量词
|
|
195
|
+
/\(\.\*[^)]*\)\s*[*+]/, // (.*)+ 类型
|
|
196
|
+
];
|
|
197
|
+
for (const dangerous of DANGEROUS_REGEX_PATTERNS) {
|
|
198
|
+
if (dangerous.test(parsed.body_pattern)) {
|
|
199
|
+
return "Rule validation failed: body_pattern contains potentially catastrophic regex";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (parsed.retry_strategy !== "fixed" && parsed.retry_strategy !== "exponential") {
|
|
203
|
+
return "retry_strategy must be 'fixed' or 'exponential'";
|
|
204
|
+
}
|
|
205
|
+
if (typeof parsed.retry_delay_ms !== "number" || !Number.isInteger(parsed.retry_delay_ms) || parsed.retry_delay_ms <= 0) {
|
|
206
|
+
return "retry_delay_ms must be a positive integer";
|
|
207
|
+
}
|
|
208
|
+
if (typeof parsed.max_retries !== "number" || !Number.isInteger(parsed.max_retries) || parsed.max_retries < 0 || parsed.max_retries > MAX_RETRIES_UPPER) {
|
|
209
|
+
return "max_retries must be 0-100";
|
|
210
|
+
}
|
|
211
|
+
if (typeof parsed.max_delay_ms !== "number" || !Number.isInteger(parsed.max_delay_ms) || parsed.max_delay_ms <= 0) {
|
|
212
|
+
return "max_delay_ms must be a positive integer";
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const MAX_PROMPT_RULES = 20;
|
|
217
|
+
/** 构造 system prompt,基于外部模板文件 + 现有规则列表 */
|
|
218
|
+
function buildSystemPrompt(existingRules) {
|
|
219
|
+
const displayRules = existingRules.slice(0, MAX_PROMPT_RULES);
|
|
220
|
+
const rulesList = displayRules.length > 0
|
|
221
|
+
? displayRules.map((r) => `- ${r.name}: status=${r.status_code}, pattern=${r.body_pattern}`).join("\n")
|
|
222
|
+
: "(none)";
|
|
223
|
+
const truncateHint = existingRules.length > MAX_PROMPT_RULES ? `\n... and ${existingRules.length - MAX_PROMPT_RULES} more rules` : "";
|
|
224
|
+
return `${AI_RETRY_PROMPT_TEMPLATE}\n\n${rulesList}${truncateHint}\n\nNote: The Response Body may be truncated. Generate body_pattern based only on the complete key-value pairs you can see.`;
|
|
225
|
+
}
|
|
226
|
+
/** 构造 user prompt,使用 provider_name 而非 provider_id */
|
|
227
|
+
function buildUserPrompt(log, responseText) {
|
|
228
|
+
const providerDisplayName = log.provider_name || log.provider_id || "unknown";
|
|
229
|
+
return `Provider: ${providerDisplayName}
|
|
230
|
+
Model: ${log.model ?? "unknown"}
|
|
231
|
+
Status Code: ${log.status_code ?? "N/A"}
|
|
232
|
+
Error Message: ${log.error_message ?? "N/A"}
|
|
233
|
+
|
|
234
|
+
Response Body:
|
|
235
|
+
${responseText}`;
|
|
236
|
+
}
|
|
37
237
|
export const adminRetryRuleRoutes = (app, options, done) => {
|
|
38
238
|
const { db, stateRegistry } = options;
|
|
39
239
|
app.get("/admin/api/retry-rules", async (_request, reply) => {
|
|
@@ -97,5 +297,122 @@ export const adminRetryRuleRoutes = (app, options, done) => {
|
|
|
97
297
|
stateRegistry?.refreshRetryRules();
|
|
98
298
|
return reply.send({ success: true });
|
|
99
299
|
});
|
|
300
|
+
const AiGenerateBodySchema = Type.Object({
|
|
301
|
+
log_id: Type.String({ minLength: 1 }),
|
|
302
|
+
});
|
|
303
|
+
// AI generate retry rule endpoint
|
|
304
|
+
app.post("/admin/api/retry-rules/ai-generate", { schema: { body: AiGenerateBodySchema } }, async (request, reply) => {
|
|
305
|
+
const { log_id } = request.body;
|
|
306
|
+
// All responses let onSend hook wrap in { code, message, data } envelope
|
|
307
|
+
// Frontend request<T>() auto-unwraps body.data
|
|
308
|
+
// 1. Check AI config
|
|
309
|
+
const aiConfigRaw = getSetting(db, "ai_retry_config");
|
|
310
|
+
if (!aiConfigRaw) {
|
|
311
|
+
return reply.send({ success: false, error: "AI retry config not set" });
|
|
312
|
+
}
|
|
313
|
+
let aiConfig;
|
|
314
|
+
try {
|
|
315
|
+
aiConfig = JSON.parse(aiConfigRaw);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return reply.send({ success: false, error: "AI config is invalid JSON" });
|
|
319
|
+
}
|
|
320
|
+
if (!aiConfig.provider_id || !aiConfig.model) {
|
|
321
|
+
return reply.send({ success: false, error: "AI config is incomplete" });
|
|
322
|
+
}
|
|
323
|
+
// 2. Look up the log
|
|
324
|
+
const log = getRequestLogById(db, log_id);
|
|
325
|
+
if (!log) {
|
|
326
|
+
return reply.send({ success: false, error: "Log not found" });
|
|
327
|
+
}
|
|
328
|
+
// 3. Extract response text
|
|
329
|
+
const responseText = extractResponseText(log);
|
|
330
|
+
// 4. Pre-check: reject 2xx responses without error features
|
|
331
|
+
const HTTP_MULTIPLE_CHOICES = 300;
|
|
332
|
+
const is2xx = log.status_code !== null && log.status_code >= HTTP_OK && log.status_code < HTTP_MULTIPLE_CHOICES;
|
|
333
|
+
if (is2xx && !log.error_message && !hasErrorFeatures(responseText)) {
|
|
334
|
+
return reply.send({ success: false, error: "Cannot generate retry rule for a successful response" });
|
|
335
|
+
}
|
|
336
|
+
// 5. Get the configured AI provider
|
|
337
|
+
const provider = getProviderById(db, aiConfig.provider_id);
|
|
338
|
+
if (!provider) {
|
|
339
|
+
return reply.send({ success: false, error: "AI provider not found" });
|
|
340
|
+
}
|
|
341
|
+
// 6. Decrypt API key
|
|
342
|
+
const encryptionKey = getSetting(db, "encryption_key");
|
|
343
|
+
if (!encryptionKey) {
|
|
344
|
+
return reply.send({ success: false, error: "Encryption key not set" });
|
|
345
|
+
}
|
|
346
|
+
let apiKey;
|
|
347
|
+
try {
|
|
348
|
+
apiKey = decrypt(provider.api_key, encryptionKey);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return reply.send({ success: false, error: "Failed to decrypt API key" });
|
|
352
|
+
}
|
|
353
|
+
// 7. Build prompts
|
|
354
|
+
const existingRules = getActiveRetryRules(db);
|
|
355
|
+
const systemPrompt = buildSystemPrompt(existingRules);
|
|
356
|
+
const userPrompt = buildUserPrompt(log, responseText);
|
|
357
|
+
// 8. Call LLM
|
|
358
|
+
let llmResult;
|
|
359
|
+
try {
|
|
360
|
+
llmResult = await callLLM({
|
|
361
|
+
baseUrl: provider.base_url,
|
|
362
|
+
upstreamPath: provider.upstream_path,
|
|
363
|
+
apiKey,
|
|
364
|
+
model: aiConfig.model,
|
|
365
|
+
messages: [
|
|
366
|
+
{ role: "system", content: systemPrompt },
|
|
367
|
+
{ role: "user", content: userPrompt },
|
|
368
|
+
],
|
|
369
|
+
maxTokens: 2048,
|
|
370
|
+
timeoutMs: 30_000,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
const msg = e instanceof Error ? e.message : "Unknown error";
|
|
375
|
+
if (!(e instanceof Error)) {
|
|
376
|
+
request.log.error({ err: e }, "LLM call failed with non-Error");
|
|
377
|
+
}
|
|
378
|
+
return reply.send({ success: false, error: `LLM call failed: ${msg}` });
|
|
379
|
+
}
|
|
380
|
+
// 9. Parse AI response
|
|
381
|
+
const parsed = parseAIContent(llmResult.content);
|
|
382
|
+
if (!parsed) {
|
|
383
|
+
// Check if the raw content is an error/refusal message
|
|
384
|
+
const lowerContent = llmResult.content.toLowerCase().trim();
|
|
385
|
+
if (lowerContent.startsWith("error") || lowerContent.includes("unable to")) {
|
|
386
|
+
return reply.send({ success: false, error: "AI returned an error exit" });
|
|
387
|
+
}
|
|
388
|
+
return reply.send({ success: false, error: "Failed to parse AI response as JSON" });
|
|
389
|
+
}
|
|
390
|
+
// 10. AI exit check — parsed object has an error field
|
|
391
|
+
if (parsed.error != null) {
|
|
392
|
+
const errorMsg = typeof parsed.error === "string"
|
|
393
|
+
? parsed.error
|
|
394
|
+
: extractErrorMessage(parsed.error);
|
|
395
|
+
return reply.send({ success: false, error: errorMsg });
|
|
396
|
+
}
|
|
397
|
+
// 11. Validate fields
|
|
398
|
+
const validationError = validateAIRule(parsed);
|
|
399
|
+
if (validationError) {
|
|
400
|
+
return reply.send({ success: false, error: `Rule validation failed: ${validationError}` });
|
|
401
|
+
}
|
|
402
|
+
// 12. Return success
|
|
403
|
+
return reply.send({
|
|
404
|
+
success: true,
|
|
405
|
+
rule: {
|
|
406
|
+
name: parsed.name,
|
|
407
|
+
status_code: parsed.status_code,
|
|
408
|
+
body_pattern: parsed.body_pattern,
|
|
409
|
+
retry_strategy: parsed.retry_strategy,
|
|
410
|
+
retry_delay_ms: parsed.retry_delay_ms,
|
|
411
|
+
max_retries: parsed.max_retries,
|
|
412
|
+
max_delay_ms: parsed.max_delay_ms,
|
|
413
|
+
},
|
|
414
|
+
summary: parsed.summary,
|
|
415
|
+
});
|
|
416
|
+
});
|
|
100
417
|
done();
|
|
101
418
|
};
|
|
@@ -18,6 +18,35 @@ export function sanitizeHeadersForLog(headers) {
|
|
|
18
18
|
}
|
|
19
19
|
return sanitized;
|
|
20
20
|
}
|
|
21
|
+
/** 从上游响应 body 中提取错误信息,用于 error_message 为空但上游返回了非 200 的场景 */
|
|
22
|
+
function extractErrorMessageFromResponse(responseBody) {
|
|
23
|
+
if (!responseBody)
|
|
24
|
+
return null;
|
|
25
|
+
const MAX_TEXT_LENGTH = 200;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(responseBody);
|
|
28
|
+
// OpenAI / DeepSeek 格式: { error: { message: "..." } }
|
|
29
|
+
const openaiMsg = parsed?.error?.message;
|
|
30
|
+
if (typeof openaiMsg === "string")
|
|
31
|
+
return openaiMsg;
|
|
32
|
+
// Cloudflare 格式: { title: "...", detail: "..." }
|
|
33
|
+
if (typeof parsed?.title === "string") {
|
|
34
|
+
const detail = parsed?.detail;
|
|
35
|
+
return typeof detail === "string" ? `${parsed.title}: ${detail}` : parsed.title;
|
|
36
|
+
}
|
|
37
|
+
// 兜底:直接 message 字段
|
|
38
|
+
if (typeof parsed?.message === "string")
|
|
39
|
+
return parsed.message;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// 非 JSON(如 HTML),截取前 200 字符
|
|
43
|
+
const text = responseBody.trim();
|
|
44
|
+
if (text.length > MAX_TEXT_LENGTH)
|
|
45
|
+
return text.slice(0, MAX_TEXT_LENGTH) + "...";
|
|
46
|
+
return text || null;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
21
50
|
// ---------- Logging helpers (extracted from proxy-core) ----------
|
|
22
51
|
// ---------- New-architecture logging ----------
|
|
23
52
|
export function logResilienceResult(db, params, attempts, result, startTime) {
|
|
@@ -77,7 +106,7 @@ export function logResilienceResult(db, params, attempts, result, startTime) {
|
|
|
77
106
|
id: attemptLogId, api_type: params.apiType, model: params.model,
|
|
78
107
|
provider_id: attempt.target.provider_id,
|
|
79
108
|
status_code: attempt.statusCode, latency_ms: attempt.latencyMs,
|
|
80
|
-
is_stream: params.isStream ? 1 : 0, error_message:
|
|
109
|
+
is_stream: params.isStream ? 1 : 0, error_message: extractErrorMessageFromResponse(attempt.responseBody),
|
|
81
110
|
created_at: new Date().toISOString(),
|
|
82
111
|
client_request: params.clientReq, upstream_request: params.upstreamReqBase,
|
|
83
112
|
upstream_response: JSON.stringify({ statusCode: attempt.statusCode, headers: attempt.responseHeaders, body: attempt.responseBody }),
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* reasoning items.
|
|
9
9
|
*/
|
|
10
10
|
import { resolveThinkingParams } from "./thinking-resolver.js";
|
|
11
|
+
import { normalizeInputTypes } from "./shared-normalize.js";
|
|
11
12
|
// ---------- Responses → Chat Completions ----------
|
|
12
13
|
/**
|
|
13
14
|
* Convert an OpenAI Responses API request body to an OpenAI Chat Completions
|
|
@@ -124,7 +125,9 @@ function convertResponsesInputToChatMessages(input, messages) {
|
|
|
124
125
|
return;
|
|
125
126
|
// Track pending function_calls to merge into a single assistant message
|
|
126
127
|
const pendingFnCalls = [];
|
|
127
|
-
|
|
128
|
+
// Codex CLI 省略 input item 的 type 字段(如 {role:"user", content:"..."}),补全为 "message"
|
|
129
|
+
const normalizedInput = normalizeInputTypes(input);
|
|
130
|
+
for (const item of normalizedInput) {
|
|
128
131
|
// Flush any pending function_calls before processing non-function_call items
|
|
129
132
|
if (item.type !== "function_call" && pendingFnCalls.length > 0) {
|
|
130
133
|
flushFunctionCalls(messages, pendingFnCalls);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sanitizeToolUseId, parseToolArguments } from "./sanitize.js";
|
|
2
|
+
import { normalizeInputTypes } from "./shared-normalize.js";
|
|
2
3
|
// ---------- Effort → budget mapping (shared with thinking-mapper) ----------
|
|
3
4
|
const EFFORT_BUDGET = { low: 1024, medium: 8192, high: 32768 };
|
|
4
5
|
const DEFAULT_BUDGET = 8192;
|
|
@@ -120,7 +121,9 @@ function convertResponsesInputToAntMessages(input) {
|
|
|
120
121
|
return { messages: [], systemParts: [] };
|
|
121
122
|
const raw = [];
|
|
122
123
|
const systemParts = [];
|
|
123
|
-
|
|
124
|
+
// Codex CLI 省略 input item 的 type 字段(如 {role:"user", content:"..."}),补全为 "message"
|
|
125
|
+
const normalizedInput = normalizeInputTypes(input);
|
|
126
|
+
for (const item of normalizedInput) {
|
|
124
127
|
if (item.type === "message") {
|
|
125
128
|
// developer/system 消息提取为 system part,不放入 messages
|
|
126
129
|
if (item.role === "developer" || item.role === "system") {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ResponseInputItem } from "./types-responses.js";
|
|
2
|
+
/**
|
|
3
|
+
* Codex CLI 省略 input item 的 type 字段(如 `{role:"user", content:"..."}`),
|
|
4
|
+
* OpenAI 官方端点静默容忍,但按 discriminated union 匹配 type 时会跳过这些 item。
|
|
5
|
+
* 补全缺失的 type 字段:有 role 但无 type 时视为 "message"。
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeInputTypes(input: ResponseInputItem[]): ResponseInputItem[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI 省略 input item 的 type 字段(如 `{role:"user", content:"..."}`),
|
|
3
|
+
* OpenAI 官方端点静默容忍,但按 discriminated union 匹配 type 时会跳过这些 item。
|
|
4
|
+
* 补全缺失的 type 字段:有 role 但无 type 时视为 "message"。
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeInputTypes(input) {
|
|
7
|
+
return input.map(item => {
|
|
8
|
+
const obj = item;
|
|
9
|
+
if (!obj.type && "role" in obj) {
|
|
10
|
+
return { ...obj, type: "message" };
|
|
11
|
+
}
|
|
12
|
+
return item;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface LLMMessage {
|
|
2
|
+
role: "system" | "user" | "assistant";
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CallLLMOptions {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
upstreamPath: string | null;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
model: string;
|
|
10
|
+
messages: LLMMessage[];
|
|
11
|
+
maxTokens?: number;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface CallLLMResult {
|
|
15
|
+
content: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function callLLM(options: CallLLMOptions): Promise<CallLLMResult>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { request as httpRequest } from "http";
|
|
2
|
+
import { request as httpsRequest } from "https";
|
|
3
|
+
const DEFAULT_STATUS_CODE = 502;
|
|
4
|
+
const HTTP_OK = 200;
|
|
5
|
+
const HTTP_MULTIPLE_CHOICES = 300;
|
|
6
|
+
const DEFAULT_UPSTREAM_PATH = "/v1/chat/completions";
|
|
7
|
+
const BYTES_PER_MB = 1_048_576;
|
|
8
|
+
const MAX_RESPONSE_MB = 5;
|
|
9
|
+
const MAX_RESPONSE_SIZE = MAX_RESPONSE_MB * BYTES_PER_MB;
|
|
10
|
+
export function callLLM(options) {
|
|
11
|
+
const path = options.upstreamPath ?? DEFAULT_UPSTREAM_PATH;
|
|
12
|
+
const url = new URL(path, options.baseUrl);
|
|
13
|
+
const requestBody = {
|
|
14
|
+
model: options.model,
|
|
15
|
+
messages: options.messages,
|
|
16
|
+
stream: false,
|
|
17
|
+
};
|
|
18
|
+
if (options.maxTokens !== undefined) {
|
|
19
|
+
requestBody.max_tokens = options.maxTokens;
|
|
20
|
+
}
|
|
21
|
+
const payload = JSON.stringify(requestBody);
|
|
22
|
+
const port = url.port ? Number(url.port) : undefined;
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const requestFn = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
25
|
+
const req = requestFn({
|
|
26
|
+
hostname: url.hostname,
|
|
27
|
+
port,
|
|
28
|
+
path: url.pathname,
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
33
|
+
"Content-Length": Buffer.byteLength(payload).toString(),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (options.timeoutMs !== undefined) {
|
|
37
|
+
req.setTimeout(options.timeoutMs, () => {
|
|
38
|
+
req.destroy(new Error("timeout"));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
req.on("response", (res) => {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let totalSize = 0;
|
|
44
|
+
res.on("data", (chunk) => {
|
|
45
|
+
totalSize += chunk.length;
|
|
46
|
+
if (totalSize > MAX_RESPONSE_SIZE) {
|
|
47
|
+
res.destroy(new Error("Response body exceeds size limit"));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
chunks.push(chunk);
|
|
51
|
+
});
|
|
52
|
+
res.on("end", () => {
|
|
53
|
+
const responseBody = Buffer.concat(chunks).toString("utf-8");
|
|
54
|
+
const statusCode = res.statusCode ?? DEFAULT_STATUS_CODE;
|
|
55
|
+
if (statusCode < HTTP_OK || statusCode >= HTTP_MULTIPLE_CHOICES) {
|
|
56
|
+
reject(new Error(`LLM API error: status code ${statusCode}`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(responseBody);
|
|
61
|
+
const content = parsed.choices?.[0]?.message?.content;
|
|
62
|
+
resolve({ content: content ?? "" });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
const ERROR_BODY_PREVIEW_LENGTH = 300;
|
|
66
|
+
const preview = responseBody.length > ERROR_BODY_PREVIEW_LENGTH
|
|
67
|
+
? responseBody.slice(0, ERROR_BODY_PREVIEW_LENGTH) + "..."
|
|
68
|
+
: responseBody;
|
|
69
|
+
reject(new Error(`Failed to parse LLM response: ${preview}`));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
res.on("error", (err) => reject(err));
|
|
73
|
+
});
|
|
74
|
+
req.on("error", (err) => reject(err));
|
|
75
|
+
req.write(payload);
|
|
76
|
+
req.end();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$ as e,Kt as t,Wt as n,ht as r,r as i,rt as a,vt as o}from"./button-
|
|
1
|
+
import{$ as e,Kt as t,Wt as n,ht as r,r as i,rt as a,vt as o}from"./button-0YdRDsHo.js";var s=[`data-size`],c=a({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(a){let c=a;return(l,u)=>(r(),e(`div`,{"data-slot":`card`,"data-size":a.size,class:t(n(i)(`ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-lg py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col`,c.class))},[o(l.$slots,`default`)],10,s))}}),l=a({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(r(),e(`div`,{"data-slot":`card-content`,class:t(n(i)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[o(a.$slots,`default`)],2))}});export{c as n,l as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$ as e,Kt as t,Wt as n,ht as r,r as i,rt as a,vt as o}from"./button-
|
|
1
|
+
import{$ as e,Kt as t,Wt as n,ht as r,r as i,rt as a,vt as o}from"./button-0YdRDsHo.js";var s=a({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(r(),e(`div`,{"data-slot":`card-header`,class:t(n(i)(`gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]`,s.class))},[o(a.$slots,`default`)],2))}}),c=a({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(r(),e(`div`,{"data-slot":`card-title`,class:t(n(i)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[o(a.$slots,`default`)],2))}});export{s as n,c as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{$ as e,I as t,It as n,Kt as r,Ot as i,Q as a,V as o,Wt as s,X as c,Y as l,Yt as u,Z as d,_t as f,ht as p,nt as m,q as h,rt as g}from"./button-0YdRDsHo.js";import{b as _,ft as v,ht as y,v as b,y as x}from"./index-Cpt-6ePL.js";var S=t(`chevron-right`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),C=t(`plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),w=[`onMouseenter`],T={class:`truncate max-w-40`},E={key:0,class:`ml-1 text-[10px] px-1 py-px rounded bg-emerald-500/15 text-emerald-400 shrink-0`},D=[`onMouseenter`],O=[`onClick`],k={class:`truncate`},A={key:0,class:`shrink-0 text-xs text-muted-foreground`},j={key:0,class:`px-2 py-1.5 text-sm text-muted-foreground`},M=g({__name:`CascadingSelect`,props:{groups:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(t,{emit:g}){let{t:y}=o(),C=t,M=l(()=>C.placeholder||y(`common.selectPlaceholder`)),N=g,P=n(!1),F=n(null),I=l(()=>{if(!C.modelValue)return``;let e=C.groups.find(e=>e.key===C.modelValue.groupKey);if(!e)return``;let t=e.options.find(e=>e.value===C.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,o)=>(p(),d(s(_),{open:P.value,"onUpdate:open":R},{default:i(()=>[m(s(b),{"as-child":``},{default:i(()=>[c(`div`,{class:r([`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`,[t.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}]])},[c(`span`,{class:r([`truncate`,t.modelValue?`text-foreground`:`text-muted-foreground`])},u(I.value||M.value),3),m(s(v),{class:`h-4 w-4 shrink-0 opacity-50`})],2)]),_:1}),m(s(x),{align:`start`,"side-offset":4,class:`z-[200] w-auto min-w-56 overflow-visible p-1`},{default:i(()=>[(p(!0),e(h,null,f(t.groups,n=>(p(),e(`div`,{key:n.key,class:r([`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},[c(`span`,T,u(n.label),1),n.badge?(p(),e(`span`,E,u(n.badge),1)):a(``,!0),m(s(S),{class:`ml-1 h-4 w-4 shrink-0 opacity-50`}),F.value===n.key&&n.options.length>0?(p(),e(`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},[(p(!0),e(h,null,f(n.options,i=>(p(),e(`div`,{key:i.value,class:r([`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":t.modelValue?.groupKey===n.key&&t.modelValue?.value===i.value}]),onClick:e=>L(n.key,i.value)},[c(`span`,k,u(i.label),1),i.tag?(p(),e(`span`,A,u(i.tag),1)):a(``,!0)],10,O))),128))],40,D)):a(``,!0)],42,w))),128)),t.groups.length===0?(p(),e(`div`,j,u(s(y)(`common.noOptions`)),1)):a(``,!0)]),_:1})]),_:1},8,[`open`]))}}),N=g({__name:`CascadingModelSelect`,props:{providers:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:t}){let{t:n}=o(),r=e,i=l(()=>r.placeholder||n(`mappings.selectProviderModel`)),a=t,s=l(()=>r.providers.map(e=>({key:e.provider.id,label:e.provider.name,badge:e.isNew?n(`common.new`):void 0,options:e.models.map(e=>({value:e.name,label:e.name,tag:y(e.contextWindow)}))}))),c=l(()=>r.modelValue?{groupKey:r.modelValue.provider_id,value:r.modelValue.model}:void 0);function u(e){a(`update:modelValue`,{provider_id:e.groupKey,model:e.value})}return(t,n)=>(p(),d(M,{groups:s.value,"model-value":c.value,placeholder:i.value,compact:e.compact,"onUpdate:modelValue":u},null,8,[`groups`,`model-value`,`placeholder`,`compact`]))}});export{C as n,N as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{G as e,K as t,Ot as n,Q as r,Wt as i,Y as a,Z as o,at as s,bt as c,ht as l,i as u,m as d,nt as f,o as p,qt as m,r as h,rt as g,ut as _,vt as v,x as y}from"./button-
|
|
1
|
+
import{G as e,K as t,Ot as n,Q as r,Wt as i,Y as a,Z as o,at as s,bt as c,ht as l,i as u,m as d,nt as f,o as p,qt as m,r as h,rt as g,ut as _,vt as v,x as y}from"./button-0YdRDsHo.js";import{t as b}from"./VisuallyHiddenInput-CRvhHFE3.js";import{t as x}from"./RovingFocusItem-BRjpvwsE.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-Cpt-6ePL.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=g({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(s,{emit:f}){let m=s,h=f,{forwardRef:g,currentElement:y}=p(),S=A(null),w=d(m,`modelValue`,h,{defaultValue:m.defaultValue??m.falseValue,passive:m.modelValue===void 0}),E=a(()=>S?.disabled.value||m.disabled),O=a(()=>D(w.value,m.trueValue)),j=a(()=>C(S?.modelValue.value)?w.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,m.value));function P(){if(C(S?.modelValue.value))w.value===`indeterminate`?w.value=m.trueValue:w.value=O.value?m.falseValue:m.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,m.value)){let t=e.findIndex(e=>D(e,m.value));e.splice(t,1)}else e.push(m.value);S.modelValue.value=e}}let I=T(y),L=a(()=>m.id&&y.value?document.querySelector(`[for="${m.id}"]`)?.innerText:void 0);return F({disabled:E,state:j}),(a,s)=>(l(),o(c(i(S)?.rovingFocus.value?i(x):i(u)),_(a.$attrs,{id:a.id,ref:i(g),role:`checkbox`,"as-child":a.asChild,as:a.as,type:a.as===`button`?`button`:void 0,"aria-checked":i(M)(j.value)?`mixed`:j.value,"aria-required":a.required,"aria-label":a.$attrs[`aria-label`]||L.value,"data-state":i(N)(j.value),"data-disabled":E.value?``:void 0,disabled:E.value,focusable:i(S)?.rovingFocus.value?!E.value:void 0,onKeydown:e(t(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:n(()=>[v(a.$slots,`default`,{modelValue:i(w),state:j.value}),i(I)&&a.name&&!i(S)?(l(),o(i(b),{key:0,type:`checkbox`,checked:!!j.value,name:a.name,value:a.value,disabled:E.value,required:a.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):r(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=g({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(e){let{forwardRef:t}=p(),r=P();return(e,a)=>(l(),o(i(w),{present:e.forceMount||i(M)(i(r).state.value)||i(r).state.value===!0},{default:n(()=>[f(i(u),_({ref:i(t),"data-state":i(N)(i(r).state.value),"data-disabled":i(r).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:n(()=>[v(e.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=g({__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:t}){let r=e,a=t,c=E(y(r,`class`),a);return(e,t)=>(l(),o(i(I),_({"data-slot":`checkbox`},i(c),{class:i(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`,r.class)}),{default:n(t=>[f(i(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:n(()=>[v(e.$slots,`default`,m(s(t)),()=>[f(i(O))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
|