llm-simple-router 0.10.13 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/model-directory.json +1 -0
- package/config/recommended-providers.json +6 -5
- package/dist/admin/groups.js +25 -0
- package/dist/admin/monitor.js +15 -6
- package/dist/admin/providers.js +22 -3
- package/dist/admin/recommended.js +13 -1
- package/dist/config/model-context.d.ts +12 -0
- package/dist/config/model-context.js +96 -2
- package/dist/config/model-directory.json +1 -0
- package/dist/config/recommended-providers.json +355 -0
- package/dist/config/recommended-retry-rules.json +12 -0
- package/dist/config/recommended.d.ts +2 -0
- package/dist/config/version.json +1 -0
- package/dist/core/monitor/request-tracker.d.ts +1 -1
- package/dist/core/monitor/request-tracker.js +2 -1
- package/dist/core/monitor/types.d.ts +1 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/index.js +17 -1
- package/dist/metrics/metrics-extractor.js +3 -0
- package/dist/proxy/handler/create-proxy-handler.js +15 -0
- package/dist/proxy/handler/failover-loop.js +88 -63
- package/dist/proxy/pipeline-snapshot.d.ts +9 -1
- package/dist/proxy/proxy-logging.js +2 -2
- package/dist/proxy/routing/modality-redirect.d.ts +22 -0
- package/dist/proxy/routing/modality-redirect.js +252 -0
- package/dist/proxy/routing/overflow.d.ts +11 -0
- package/dist/proxy/routing/overflow.js +24 -0
- package/dist/proxy/transform/plugin-registry.js +1 -1
- package/dist/proxy/transform/stream-oa2ant.js +3 -0
- package/dist/proxy/transport/http.js +6 -0
- package/dist/proxy/transport/proxy-agent.js +20 -8
- package/dist/proxy/transport/stream.js +8 -1
- package/dist/proxy/transport/transport-fn.js +12 -0
- package/frontend-dist/assets/CardContent-yiYaxAko.js +1 -0
- package/frontend-dist/assets/CardTitle-CzqSlrtn.js +1 -0
- package/frontend-dist/assets/Checkbox-2voapLgE.js +1 -0
- package/frontend-dist/assets/CollapsibleContent-DHkVSWt2.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-DbVCeTdD.js +1 -0
- package/frontend-dist/assets/Dashboard-xT1CEwOR.js +3 -0
- package/frontend-dist/assets/{Input-Ey_q_5_r.js → Input-DEfnoFS3.js} +1 -1
- package/frontend-dist/assets/Label-CjUuzGNQ.js +1 -0
- package/frontend-dist/assets/Login-CJDEk-tO.js +1 -0
- package/frontend-dist/assets/Logs-CzdPCIYV.js +1 -0
- package/frontend-dist/assets/MappingEntryEditor-GejG6FYv.js +1 -0
- package/frontend-dist/assets/ModelCard-DdQtySPM.js +1 -0
- package/frontend-dist/assets/ModelMappings-DffY7Izx.js +1 -0
- package/frontend-dist/assets/Monitor-y6d6LInm.js +1 -0
- package/frontend-dist/assets/Providers-Cb-CB1yf.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-CywRxDop.js +1 -0
- package/frontend-dist/assets/QuickSetup-Nj_ysAdc.js +1 -0
- package/frontend-dist/assets/RetryRules-DRdeZUPt.js +1 -0
- package/frontend-dist/assets/RouterKeys-BHOhDgXZ.js +1 -0
- package/frontend-dist/assets/RovingFocusItem-NxZWBEpr.js +1 -0
- package/frontend-dist/assets/Schedules-C4jRCbnI.js +1 -0
- package/frontend-dist/assets/Settings-Cn0qnqMY.js +6 -0
- package/frontend-dist/assets/Setup-BjN6KU0y.js +1 -0
- package/frontend-dist/assets/Switch-bk3eQSZ_.js +1 -0
- package/frontend-dist/assets/TooltipTrigger-DmYucHtv.js +1 -0
- package/frontend-dist/assets/TransformRulesForm-Bo-zFABv.js +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-5-vBmVMH.js +3 -0
- package/frontend-dist/assets/VisuallyHiddenInput-BflIWQCW.js +1 -0
- package/frontend-dist/assets/{button-C7HO6Dyb.js → button-DZwflOXO.js} +2 -2
- package/frontend-dist/assets/{copy-DxwFlq2A.js → copy-zQQvOqam.js} +1 -1
- package/frontend-dist/assets/dialog-C7v6Gaak.js +1 -0
- package/frontend-dist/assets/index-ClQS69Or.css +1 -0
- package/frontend-dist/assets/index-PMAQyWJb.js +3 -0
- package/frontend-dist/assets/mappings-BpkOqnsu.js +1 -0
- package/frontend-dist/assets/mappings-D7Qy46v_.js +1 -0
- package/frontend-dist/assets/{providers-Bcea72GK.js → providers-BI5dO-j0.js} +1 -1
- package/frontend-dist/assets/{providers-DNICB6Kg.js → providers-BzxbZ85B.js} +1 -1
- package/frontend-dist/assets/{trash-2-D2SrfECO.js → trash-2-CrcHK-G_.js} +1 -1
- package/frontend-dist/assets/{useClipboard-CttzUerj.js → useClipboard-B4K3eogm.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-Dv0deAan.js → useLogRetention-BNbFXLBO.js} +1 -1
- package/frontend-dist/index.html +3 -3
- package/package.json +2 -2
- package/frontend-dist/assets/CardContent-DfVo-N85.js +0 -1
- package/frontend-dist/assets/CardTitle-npwJSAlz.js +0 -1
- package/frontend-dist/assets/Checkbox-Ddnzkh_i.js +0 -1
- package/frontend-dist/assets/CollapsibleContent-BTVazeoQ.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-DCQeyHrt.js +0 -1
- package/frontend-dist/assets/Dashboard-DjnImtwH.js +0 -3
- package/frontend-dist/assets/Label-Dw5HcYsL.js +0 -1
- package/frontend-dist/assets/Login-CSrfhhm9.js +0 -1
- package/frontend-dist/assets/Logs-HR1DZs1M.js +0 -1
- package/frontend-dist/assets/MappingEntryEditor-C9pgNL0Q.js +0 -1
- package/frontend-dist/assets/ModelCard-IQMwlnCm.js +0 -1
- package/frontend-dist/assets/ModelMappings-kRx-GL_7.js +0 -1
- package/frontend-dist/assets/Monitor-y1ofDNK7.js +0 -1
- package/frontend-dist/assets/Providers-C1bP2PoM.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-DQx4coxn.js +0 -1
- package/frontend-dist/assets/QuickSetup-DHX9-CnO.js +0 -1
- package/frontend-dist/assets/RetryRules-zdJE0bFL.js +0 -1
- package/frontend-dist/assets/RouterKeys-CD0rI4kv.js +0 -1
- package/frontend-dist/assets/RovingFocusItem-CFmjbm49.js +0 -1
- package/frontend-dist/assets/Schedules-BUm3cC6w.js +0 -1
- package/frontend-dist/assets/Settings-D7z5IRkY.js +0 -6
- package/frontend-dist/assets/Setup-i9inmgjB.js +0 -1
- package/frontend-dist/assets/Switch-C9DeYAnK.js +0 -1
- package/frontend-dist/assets/TooltipTrigger-Dr6kqGSH.js +0 -1
- package/frontend-dist/assets/TransformRulesForm-CyXh4jHa.js +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-6ZRBfjko.js +0 -3
- package/frontend-dist/assets/VisuallyHiddenInput-CwE9jREu.js +0 -1
- package/frontend-dist/assets/constants-yM0YwP2s.js +0 -1
- package/frontend-dist/assets/dialog-BWB1aLcT.js +0 -1
- package/frontend-dist/assets/index-DeeDpH_W.css +0 -1
- package/frontend-dist/assets/index-itL9--Q_.js +0 -3
- package/frontend-dist/assets/mappings-6w7mc8YK.js +0 -1
- package/frontend-dist/assets/mappings-C1fK_e70.js +0 -1
- /package/frontend-dist/assets/{common-D96jEq-h.js → common-Bvxev9Ev.js} +0 -0
- /package/frontend-dist/assets/{common-BpwAv-lj.js → common-Cn0QcrnY.js} +0 -0
- /package/frontend-dist/assets/{dashboard-DjgmcUG5.js → dashboard-Cejt1wVQ.js} +0 -0
- /package/frontend-dist/assets/{dashboard-COCyp2p_.js → dashboard-DLTOR0fN.js} +0 -0
- /package/frontend-dist/assets/{login-BTNL5nN5.js → login-BkOvA7gg.js} +0 -0
- /package/frontend-dist/assets/{login-Sef1i0de.js → login-DWRFsEu3.js} +0 -0
- /package/frontend-dist/assets/{logs-CBRLywRw.js → logs-CA8USnXG.js} +0 -0
- /package/frontend-dist/assets/{logs-B-6cgV12.js → logs-QPt2Ybwy.js} +0 -0
- /package/frontend-dist/assets/{monitor-CaDMr_KG.js → monitor-CcPZdXUM.js} +0 -0
- /package/frontend-dist/assets/{monitor-C9j7ppMj.js → monitor-D-0KOVTC.js} +0 -0
- /package/frontend-dist/assets/{proxyEnhancement-DpIVSv-g.js → proxyEnhancement-B6vdsMeK.js} +0 -0
- /package/frontend-dist/assets/{proxyEnhancement-rSM6KhbN.js → proxyEnhancement-UuPFs4M3.js} +0 -0
- /package/frontend-dist/assets/{quickSetup-CCxaqY3U.js → quickSetup-CSpWmAy-.js} +0 -0
- /package/frontend-dist/assets/{quickSetup-DgDENHE4.js → quickSetup-D8ruRelW.js} +0 -0
- /package/frontend-dist/assets/{requestDetail-DZ55ph4h.js → requestDetail-8Sp9tWNb.js} +0 -0
- /package/frontend-dist/assets/{requestDetail-3KCtYe1N.js → requestDetail-CcHzzKYr.js} +0 -0
- /package/frontend-dist/assets/{retryRules-BXrRL52J.js → retryRules-C--dd-y8.js} +0 -0
- /package/frontend-dist/assets/{retryRules-CToGC6cR.js → retryRules-CzLnagW_.js} +0 -0
- /package/frontend-dist/assets/{routerKeys-DbTg4OP1.js → routerKeys-CB2l_V7w.js} +0 -0
- /package/frontend-dist/assets/{routerKeys-Be7OZCn0.js → routerKeys-p_ioAckE.js} +0 -0
- /package/frontend-dist/assets/{schedules-Bd66RL7P.js → schedules-Cz_-Wfa_.js} +0 -0
- /package/frontend-dist/assets/{schedules-HDwMuDgX.js → schedules-DTgk603B.js} +0 -0
- /package/frontend-dist/assets/{settings-DCS-RTKl.js → settings-B5Mq1HN8.js} +0 -0
- /package/frontend-dist/assets/{settings-C4zZB9GY.js → settings-j3dzVXzy.js} +0 -0
- /package/frontend-dist/assets/{setup-CrjgRrYP.js → setup-DaeEG9ll.js} +0 -0
- /package/frontend-dist/assets/{setup-DmgXvgkY.js → setup-Dryg-9wL.js} +0 -0
- /package/frontend-dist/assets/{sidebar-3c8D7l60.js → sidebar-BQWT-QZb.js} +0 -0
- /package/frontend-dist/assets/{sidebar-vj4kQ6t1.js → sidebar-DYwEKca3.js} +0 -0
|
@@ -94,11 +94,12 @@
|
|
|
94
94
|
"group": "智谱",
|
|
95
95
|
"presets": [
|
|
96
96
|
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
"plan": "Coding Plan",
|
|
98
|
+
"presetName": "zhipu-coding-plan",
|
|
99
|
+
"apiType": "openai",
|
|
100
|
+
"baseUrl": "https://open.bigmodel.cn",
|
|
101
|
+
"upstreamPath": "/api/coding/paas/v4/chat/completions",
|
|
102
|
+
"modelsEndpoint": "/v1/models",
|
|
102
103
|
"models": [
|
|
103
104
|
"glm-5.1",
|
|
104
105
|
"glm-5",
|
package/dist/admin/groups.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getProviderById, getMappingGroupById, } from "../db/index.js";
|
|
3
3
|
import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_CONFLICT, HTTP_NOT_FOUND } from "./constants.js";
|
|
4
|
+
import { parseModels } from "../config/model-context.js";
|
|
4
5
|
import { API_CODE, apiError } from "./api-response.js";
|
|
5
6
|
const CreateGroupSchema = Type.Object({
|
|
6
7
|
client_model: Type.String({ minLength: 1 }),
|
|
@@ -54,6 +55,30 @@ function validateRule(db, ruleJson) {
|
|
|
54
55
|
if (overflowErr)
|
|
55
56
|
return overflowErr;
|
|
56
57
|
}
|
|
58
|
+
// Validate multimodal_fallback if present
|
|
59
|
+
const fallback = r.multimodal_fallback;
|
|
60
|
+
if (fallback !== undefined && fallback !== null) {
|
|
61
|
+
const fb = fallback;
|
|
62
|
+
if (!fb.provider_id) {
|
|
63
|
+
return "multimodal_fallback: provider_id is required";
|
|
64
|
+
}
|
|
65
|
+
if (!fb.backend_model) {
|
|
66
|
+
return "multimodal_fallback: backend_model is required";
|
|
67
|
+
}
|
|
68
|
+
const fbProvider = getProviderById(db, fb.provider_id);
|
|
69
|
+
if (!fbProvider) {
|
|
70
|
+
return `multimodal_fallback: provider_id '${fb.provider_id}' not found`;
|
|
71
|
+
}
|
|
72
|
+
if (fbProvider.is_active !== 1) {
|
|
73
|
+
return `multimodal_fallback: provider '${fbProvider.name}' is not active`;
|
|
74
|
+
}
|
|
75
|
+
// 校验 backend_model 是否在 provider 的 models 列表中
|
|
76
|
+
const providerModels = parseModels(fbProvider.models);
|
|
77
|
+
const modelExists = providerModels.some(m => m.name === fb.backend_model);
|
|
78
|
+
if (!modelExists) {
|
|
79
|
+
return `multimodal_fallback: backend_model '${fb.backend_model}' not found in provider '${fbProvider.name}' models list`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
57
82
|
return undefined;
|
|
58
83
|
}
|
|
59
84
|
export const adminGroupRoutes = (app, options, done) => {
|
package/dist/admin/monitor.js
CHANGED
|
@@ -16,16 +16,25 @@ export const adminMonitorRoutes = (app, options, done) => {
|
|
|
16
16
|
app.get("/admin/api/monitor/stream", (request, reply) => {
|
|
17
17
|
// hijack() 让 Fastify 完全放弃响应管理,避免 onSend hook 向 SSE 流注入信封 JSON
|
|
18
18
|
reply.hijack();
|
|
19
|
-
reply.raw.writeHead(HTTP_OK, {
|
|
20
|
-
"Content-Type": "text/event-stream",
|
|
21
|
-
"Cache-Control": "no-cache",
|
|
22
|
-
Connection: "keep-alive",
|
|
23
|
-
});
|
|
24
19
|
const sseClient = adaptSSEClient(reply.raw);
|
|
25
20
|
tracker.addClient(sseClient);
|
|
26
|
-
|
|
21
|
+
// 在 writeHead 之前注册 close 处理器,避免竞态导致 tracker 泄漏
|
|
22
|
+
reply.raw.on("close", () => {
|
|
27
23
|
tracker.removeClient(sseClient);
|
|
28
24
|
});
|
|
25
|
+
// 客户端在 hijack 之前已断连,无需发送响应头
|
|
26
|
+
if (reply.raw.destroyed)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
reply.raw.writeHead(HTTP_OK, {
|
|
30
|
+
"Content-Type": "text/event-stream",
|
|
31
|
+
"Cache-Control": "no-cache",
|
|
32
|
+
Connection: "keep-alive",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
request.log.debug("client disconnected before writeHead");
|
|
37
|
+
}
|
|
29
38
|
});
|
|
30
39
|
app.get("/admin/api/monitor/request/:id", async (request, reply) => {
|
|
31
40
|
const { id } = request.params;
|
package/dist/admin/providers.js
CHANGED
|
@@ -74,6 +74,8 @@ function extractModelOverrides(models) {
|
|
|
74
74
|
const entry = { name, patches: m.patches ?? [] };
|
|
75
75
|
if (m.stream_timeout_ms != null)
|
|
76
76
|
entry.stream_timeout_ms = m.stream_timeout_ms;
|
|
77
|
+
if (m.capabilities != null && Array.isArray(m.capabilities))
|
|
78
|
+
entry.capabilities = m.capabilities;
|
|
77
79
|
entries.push(entry);
|
|
78
80
|
if (m.name != null && m.context_window != null) {
|
|
79
81
|
overrides.push({ name: m.name, context_window: m.context_window });
|
|
@@ -83,6 +85,16 @@ function extractModelOverrides(models) {
|
|
|
83
85
|
}
|
|
84
86
|
const API_KEY_PREVIEW_PREFIX_LEN = 4;
|
|
85
87
|
const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
88
|
+
/** 校验 base_url 是否为合法的 HTTP(S) URL */
|
|
89
|
+
function isValidHttpUrl(str) {
|
|
90
|
+
try {
|
|
91
|
+
const url = new URL(str);
|
|
92
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
86
98
|
const CreateProviderSchema = Type.Object({
|
|
87
99
|
name: Type.String({ minLength: 1 }),
|
|
88
100
|
api_type: Type.Union([Type.Literal("openai"), Type.Literal("anthropic")]),
|
|
@@ -91,7 +103,7 @@ const CreateProviderSchema = Type.Object({
|
|
|
91
103
|
api_key: Type.String({ minLength: 1 }),
|
|
92
104
|
models: Type.Optional(Type.Array(Type.Union([
|
|
93
105
|
Type.String(),
|
|
94
|
-
Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) }),
|
|
106
|
+
Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })), capabilities: Type.Optional(Type.Array(Type.String())) }),
|
|
95
107
|
Type.Object({ id: Type.String(), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) })
|
|
96
108
|
]))),
|
|
97
109
|
is_active: Type.Optional(Type.Number()),
|
|
@@ -112,7 +124,7 @@ const UpdateProviderSchema = Type.Object({
|
|
|
112
124
|
api_key: Type.Optional(Type.String({ minLength: 1 })),
|
|
113
125
|
models: Type.Optional(Type.Array(Type.Union([
|
|
114
126
|
Type.String(),
|
|
115
|
-
Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) }),
|
|
127
|
+
Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })), capabilities: Type.Optional(Type.Array(Type.String())) }),
|
|
116
128
|
Type.Object({ id: Type.String(), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) })
|
|
117
129
|
]))),
|
|
118
130
|
is_active: Type.Optional(Type.Number()),
|
|
@@ -165,6 +177,9 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
165
177
|
if (existing) {
|
|
166
178
|
return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, `Provider 名称 '${body.name}' 已存在`));
|
|
167
179
|
}
|
|
180
|
+
if (!isValidHttpUrl(body.base_url)) {
|
|
181
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "base_url 格式无效,必须是以 http:// 或 https:// 开头的合法 URL"));
|
|
182
|
+
}
|
|
168
183
|
const encryptedKey = encrypt(body.api_key, getSetting(db, "encryption_key"));
|
|
169
184
|
const { entries: normalizedModels, overrides: contextOverrides } = extractModelOverrides((body.models ?? []));
|
|
170
185
|
const isAdaptiveEnabled = body.adaptive_enabled ?? 0;
|
|
@@ -231,8 +246,12 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
231
246
|
fields.name = body.name;
|
|
232
247
|
if (body.api_type !== undefined)
|
|
233
248
|
fields.api_type = body.api_type;
|
|
234
|
-
if (body.base_url !== undefined)
|
|
249
|
+
if (body.base_url !== undefined) {
|
|
250
|
+
if (!isValidHttpUrl(body.base_url)) {
|
|
251
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "base_url 格式无效,必须是以 http:// 或 https:// 开头的合法 URL"));
|
|
252
|
+
}
|
|
235
253
|
fields.base_url = body.base_url;
|
|
254
|
+
}
|
|
236
255
|
if (body.upstream_path !== undefined)
|
|
237
256
|
fields.upstream_path = body.upstream_path || null;
|
|
238
257
|
if (body.is_active !== undefined)
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { getRecommendedProviders, getRecommendedRetryRules, reloadConfig } from "../config/recommended.js";
|
|
2
|
+
import { lookupCapabilities } from "../config/model-context.js";
|
|
2
3
|
export const adminRecommendedRoutes = (app, options, done) => {
|
|
3
4
|
const { db } = options;
|
|
4
5
|
app.get("/admin/api/recommended/providers", async (_req, reply) => {
|
|
5
|
-
|
|
6
|
+
const groups = getRecommendedProviders();
|
|
7
|
+
// 给每个预设的模型补上 capabilities
|
|
8
|
+
for (const group of groups) {
|
|
9
|
+
for (const preset of group.presets) {
|
|
10
|
+
const capMap = {};
|
|
11
|
+
for (const m of preset.models) {
|
|
12
|
+
capMap[m] = lookupCapabilities(m);
|
|
13
|
+
}
|
|
14
|
+
preset.modelCapabilities = capMap;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return reply.send(groups);
|
|
6
18
|
});
|
|
7
19
|
app.get("/admin/api/recommended/retry-rules", async (_req, reply) => {
|
|
8
20
|
const rules = getRecommendedRetryRules();
|
|
@@ -3,16 +3,28 @@ export interface ModelInfo {
|
|
|
3
3
|
context_window: number | null;
|
|
4
4
|
patches: string[];
|
|
5
5
|
stream_timeout_ms?: number;
|
|
6
|
+
capabilities?: string[];
|
|
6
7
|
}
|
|
7
8
|
export interface ModelEntry {
|
|
8
9
|
name: string;
|
|
9
10
|
context_window?: number;
|
|
10
11
|
patches?: string[];
|
|
11
12
|
stream_timeout_ms?: number;
|
|
13
|
+
capabilities?: string[];
|
|
12
14
|
}
|
|
13
15
|
export declare const MODEL_CONTEXT_WINDOWS: Record<string, number>;
|
|
16
|
+
/** 已知支持图片输入的模型白名单。不在表中的模型默认 [\"text\"]。 */
|
|
17
|
+
export declare const MODEL_CAPABILITIES: Record<string, string[]>;
|
|
14
18
|
export declare const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
15
19
|
export declare const OVERFLOW_THRESHOLD = 1000000;
|
|
20
|
+
/**
|
|
21
|
+
* 加载 config/model-directory.json(由 sync-model-directory.sh 生成)。
|
|
22
|
+
* 加载失败时不覆盖默认值,fallback 到硬编码白名单。
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadModelDirectory(configDir?: string): void;
|
|
25
|
+
/** 查询模型 capabilities:显式配置 > model-directory.json > 硬编码白名单 > ["text"] */
|
|
26
|
+
export declare function lookupCapabilities(modelName: string): string[];
|
|
27
|
+
/** 查询模型上下文窗口:model-directory.json > 硬编码表 > 默认值 */
|
|
16
28
|
export declare function lookupContextWindow(modelName: string): number;
|
|
17
29
|
/** 标准化 patch 名称:连字符 → 下划线 */
|
|
18
30
|
export declare function normalizePatchName(name: string): string;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
export const MODEL_CONTEXT_WINDOWS = {
|
|
2
4
|
// DeepSeek
|
|
3
5
|
"deepseek-chat": 1000000,
|
|
@@ -79,10 +81,96 @@ export const MODEL_CONTEXT_WINDOWS = {
|
|
|
79
81
|
"moonshotai/Kimi-K2-Instruct": 128000,
|
|
80
82
|
"moonshotai/Kimi-K2.5": 256000,
|
|
81
83
|
};
|
|
84
|
+
/** 已知支持图片输入的模型白名单。不在表中的模型默认 [\"text\"]。 */
|
|
85
|
+
export const MODEL_CAPABILITIES = {
|
|
86
|
+
// ── OpenAI ── 文档确认支持 image_url
|
|
87
|
+
"gpt-4o": ["text", "image"],
|
|
88
|
+
"gpt-4o-mini": ["text", "image"],
|
|
89
|
+
"gpt-4-turbo": ["text", "image"],
|
|
90
|
+
"gpt-4.1": ["text", "image"],
|
|
91
|
+
"gpt-4.1-mini": ["text", "image"],
|
|
92
|
+
"gpt-4.1-nano": ["text", "image"],
|
|
93
|
+
"o1": ["text", "image"],
|
|
94
|
+
"o1-pro": ["text", "image"],
|
|
95
|
+
"o3": ["text", "image"],
|
|
96
|
+
"o3-mini": ["text", "image"],
|
|
97
|
+
"o4-mini": ["text", "image"],
|
|
98
|
+
// ── Anthropic ── 文档确认支持 image content block
|
|
99
|
+
"claude-3.5-sonnet": ["text", "image"],
|
|
100
|
+
"claude-3.5-haiku": ["text", "image"],
|
|
101
|
+
"claude-3-opus": ["text", "image"],
|
|
102
|
+
"claude-4-sonnet": ["text", "image"],
|
|
103
|
+
"claude-4-opus": ["text", "image"],
|
|
104
|
+
// ── DeepSeek ──
|
|
105
|
+
// V3/V4 不接受 OpenAI image_url 格式(API 返回 unknown variant 'image_url')
|
|
106
|
+
// 只有专用视觉模型 deepseek-vl2 支持
|
|
107
|
+
"deepseek-vl2": ["text", "image"],
|
|
108
|
+
// ── 智谱 ──
|
|
109
|
+
// GLM-5/5.1 是纯文本 LLM;GLM-5V-Turbo / GLM-4.5V 才是视觉模型
|
|
110
|
+
// 文档确认视觉模型支持 image_url 格式
|
|
111
|
+
"glm-5v-turbo": ["text", "image", "audio", "video"],
|
|
112
|
+
"glm-4.5v": ["text", "image"],
|
|
113
|
+
"glm-4v-plus": ["text", "image"],
|
|
114
|
+
"glm-4v-flash": ["text", "image"],
|
|
115
|
+
// ── 月之暗面 ── 原生多模态架构,全部支持 image_url
|
|
116
|
+
"moonshot-v1-128k": ["text", "image"],
|
|
117
|
+
"moonshot-v1-32k": ["text", "image"],
|
|
118
|
+
"moonshot-v1-8k": ["text", "image"],
|
|
119
|
+
"kimi-k2.6": ["text", "image", "video"],
|
|
120
|
+
"kimi-k2.5": ["text", "image", "video"],
|
|
121
|
+
"kimi-k2-turbo-preview": ["text", "image"],
|
|
122
|
+
"kimi-k2-thinking": ["text", "image"],
|
|
123
|
+
"kimi-for-coding": ["text", "image"],
|
|
124
|
+
// ── 阿里云 Qwen ── 百炼文档确认 qwen3.6-plus/qwen3.5-plus/flash 支持 image_url
|
|
125
|
+
"qwen-vl-max": ["text", "image"],
|
|
126
|
+
"qwen-vl-plus": ["text", "image"],
|
|
127
|
+
"qwen3.6-plus": ["text", "image", "video"],
|
|
128
|
+
"qwen3.5-plus": ["text", "image", "video"],
|
|
129
|
+
"qwen3.5-flash": ["text", "image"],
|
|
130
|
+
// ── 火山引擎 ── Doubao Seed 2.0 Pro 规格:Input Text, Images, Video
|
|
131
|
+
"doubao-seed-2-0-pro-260215": ["text", "image", "video"],
|
|
132
|
+
// ── 小米 MiMo ── 只有 omni 版本支持图片,pro 版本是纯文本
|
|
133
|
+
"mimo-v2-omni": ["text", "image", "audio", "video"],
|
|
134
|
+
"mimo-v2.5": ["text", "image", "audio", "video"],
|
|
135
|
+
};
|
|
82
136
|
export const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
83
137
|
export const OVERFLOW_THRESHOLD = 1000000;
|
|
138
|
+
let directoryCapabilities = {};
|
|
139
|
+
let directoryContextWindows = {};
|
|
140
|
+
/**
|
|
141
|
+
* 加载 config/model-directory.json(由 sync-model-directory.sh 生成)。
|
|
142
|
+
* 加载失败时不覆盖默认值,fallback 到硬编码白名单。
|
|
143
|
+
*/
|
|
144
|
+
export function loadModelDirectory(configDir) {
|
|
145
|
+
try {
|
|
146
|
+
const dir = configDir ?? path.resolve(process.cwd(), "config");
|
|
147
|
+
const filePath = path.join(dir, "model-directory.json");
|
|
148
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
149
|
+
const data = JSON.parse(raw);
|
|
150
|
+
if (data.capabilities && typeof data.capabilities === "object") {
|
|
151
|
+
directoryCapabilities = data.capabilities;
|
|
152
|
+
}
|
|
153
|
+
if (data.context_windows && typeof data.context_windows === "object") {
|
|
154
|
+
directoryContextWindows = data.context_windows;
|
|
155
|
+
}
|
|
156
|
+
// eslint-disable-next-line taste/no-silent-catch -- 加载失败不影响启动,使用硬编码白名单兆底。但记录到 stderr 供诊断
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
// 加载失败不影响启动,使用硬编码白名单兆底。但记录到 stderr 供诊断
|
|
160
|
+
console.error('loadModelDirectory: failed to load, using hardcoded fallback', err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** 查询模型 capabilities:显式配置 > model-directory.json > 硬编码白名单 > ["text"] */
|
|
164
|
+
export function lookupCapabilities(modelName) {
|
|
165
|
+
return MODEL_CAPABILITIES[modelName]
|
|
166
|
+
?? directoryCapabilities[modelName]
|
|
167
|
+
?? ["text"];
|
|
168
|
+
}
|
|
169
|
+
/** 查询模型上下文窗口:model-directory.json > 硬编码表 > 默认值 */
|
|
84
170
|
export function lookupContextWindow(modelName) {
|
|
85
|
-
return MODEL_CONTEXT_WINDOWS[modelName]
|
|
171
|
+
return MODEL_CONTEXT_WINDOWS[modelName]
|
|
172
|
+
?? directoryContextWindows[modelName]
|
|
173
|
+
?? DEFAULT_CONTEXT_WINDOW;
|
|
86
174
|
}
|
|
87
175
|
/** 标准化 patch 名称:连字符 → 下划线 */
|
|
88
176
|
export function normalizePatchName(name) {
|
|
@@ -122,7 +210,9 @@ export function parseModels(raw) {
|
|
|
122
210
|
return [];
|
|
123
211
|
const result = parsed.map((item) => {
|
|
124
212
|
if (typeof item === 'string') {
|
|
125
|
-
return item
|
|
213
|
+
return item
|
|
214
|
+
? { name: item, patches: [], capabilities: lookupCapabilities(item) }
|
|
215
|
+
: null;
|
|
126
216
|
}
|
|
127
217
|
const obj = item;
|
|
128
218
|
if (!obj)
|
|
@@ -139,6 +229,8 @@ export function parseModels(raw) {
|
|
|
139
229
|
};
|
|
140
230
|
if (obj.stream_timeout_ms != null)
|
|
141
231
|
entry.stream_timeout_ms = obj.stream_timeout_ms;
|
|
232
|
+
// capabilities: 显式 > model-directory > 硬编码白名单 > 默认 ["text"]
|
|
233
|
+
entry.capabilities = obj.capabilities ?? lookupCapabilities(modelName);
|
|
142
234
|
return entry;
|
|
143
235
|
}).filter((e) => e !== null);
|
|
144
236
|
modelsCache.set(raw, result);
|
|
@@ -157,6 +249,8 @@ export function buildModelInfoList(modelEntries, overrides) {
|
|
|
157
249
|
};
|
|
158
250
|
if (entry.stream_timeout_ms != null)
|
|
159
251
|
info.stream_timeout_ms = entry.stream_timeout_ms;
|
|
252
|
+
if (entry.capabilities != null)
|
|
253
|
+
info.capabilities = entry.capabilities;
|
|
160
254
|
return info;
|
|
161
255
|
});
|
|
162
256
|
}
|