llm-simple-router 0.11.28 → 0.11.29
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/recommended-providers.json +23 -7
- package/dist/admin/providers.js +164 -69
- package/dist/config/recommended-providers.json +23 -7
- package/dist/config/recommended.d.ts +2 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/db/logs.d.ts +4 -0
- package/dist/db/logs.js +6 -3
- package/dist/db/migrations/051_provider_endpoints.sql +15 -0
- package/dist/db/migrations/052_add_upstream_log_fields.sql +3 -0
- package/dist/db/providers.d.ts +9 -1
- package/dist/db/providers.js +32 -3
- package/dist/index.js +13 -0
- package/dist/proxy/handler/failover-loop.d.ts +136 -1
- package/dist/proxy/handler/failover-loop.js +395 -299
- package/dist/proxy/log-helpers.d.ts +4 -0
- package/dist/proxy/log-helpers.js +6 -2
- package/dist/proxy/proxy-logging.d.ts +2 -0
- package/dist/proxy/proxy-logging.js +2 -0
- package/dist/proxy/routing/resolve-endpoint.d.ts +3 -0
- package/dist/proxy/routing/resolve-endpoint.js +38 -0
- package/dist/proxy/transform/types.d.ts +1 -2
- package/dist/proxy/transport/transport-fn.d.ts +1 -0
- package/dist/proxy/transport/transport-fn.js +1 -1
- package/frontend-dist/assets/{CardContent-BbiKtIeE.js → CardContent-CPgJkYqm.js} +1 -1
- package/frontend-dist/assets/{CardTitle-BKzqpj_c.js → CardTitle-DlmZKWtW.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-B7lZ1mP0.js → CascadingModelSelect-BwPgKnOc.js} +1 -1
- package/frontend-dist/assets/{Checkbox-D1Y3kGGA.js → Checkbox-BV5EQ4YM.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-CoARuSxM.js → CollapsibleContent-urA_6kfp.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-D2zhWvtd.js → CollapsibleTrigger-DJRieUOm.js} +1 -1
- package/frontend-dist/assets/{Dashboard-CU8be5NR.js → Dashboard-DSwRrmn8.js} +1 -1
- package/frontend-dist/assets/{Input-BOlb6Kuw.js → Input-DERHUkRk.js} +1 -1
- package/frontend-dist/assets/{Label-CCZ5BOi9.js → Label-D5PFB0st.js} +1 -1
- package/frontend-dist/assets/{Login-Dux0E-6K.js → Login-KolKJnRj.js} +1 -1
- package/frontend-dist/assets/Logs-D4tcVJdx.js +1 -0
- package/frontend-dist/assets/{MappingEntryEditor-BPqzjEiL.js → MappingEntryEditor-D4Te5uWj.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-BT_Cpt-a.js → ModelMappings-CR92Hl4D.js} +1 -1
- package/frontend-dist/assets/{Monitor-tOgUttQg.js → Monitor-Ccpd95Lb.js} +1 -1
- package/frontend-dist/assets/Providers-CN-w_3iZ.js +1 -0
- package/frontend-dist/assets/{ProxyEnhancement-BbBy7nrm.js → ProxyEnhancement-aDCbph8B.js} +1 -1
- package/frontend-dist/assets/QuickSetup-BgrY5WgS.js +1 -0
- package/frontend-dist/assets/{RetryRules-DfEaQ1YN.js → RetryRules-Ck7GDfvp.js} +1 -1
- package/frontend-dist/assets/{RouterKeys--rpRgKSn.js → RouterKeys-KG3Hiqle.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-CvIxVLbB.js → RovingFocusItem-CwTuwFla.js} +1 -1
- package/frontend-dist/assets/{Schedules-B7EFQyXL.js → Schedules-BiuKtYN_.js} +1 -1
- package/frontend-dist/assets/{Settings-BvKMJ0Qa.js → Settings-hbCMg8D_.js} +1 -1
- package/frontend-dist/assets/{Setup-4vmVajOU.js → Setup-C-Sdhz4l.js} +1 -1
- package/frontend-dist/assets/{Switch-D9H1FPsB.js → Switch-CjhSCxdv.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-Btj1Vsxm.js → TooltipTrigger-WXdFCeVq.js} +1 -1
- package/frontend-dist/assets/{TransformRulesForm-BJ4caGDj.js → TransformRulesForm-BXo7UEUm.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-BbQBSoU7.js → UnifiedRequestDialog-D9UChyhv.js} +3 -3
- package/frontend-dist/assets/{VisuallyHiddenInput-BoPTforN.js → VisuallyHiddenInput-CkyFdj9S.js} +1 -1
- package/frontend-dist/assets/{button-wn0cCDld.js → button-BTtVoe27.js} +2 -2
- package/frontend-dist/assets/{copy-DBpQ3kDg.js → copy-BRzaTo_t.js} +1 -1
- package/frontend-dist/assets/{dialog-DCaFnzS6.js → dialog-BO9QSeKn.js} +1 -1
- package/frontend-dist/assets/index-B_tA9Si9.css +1 -0
- package/frontend-dist/assets/{index-Bp-3UWSJ.js → index-CN2qglG7.js} +2 -2
- package/frontend-dist/assets/model-patches-9dYvtlVH.js +1 -0
- package/frontend-dist/assets/plus-D34D5D4i.js +1 -0
- package/frontend-dist/assets/providers-BaiCysMg.js +1 -0
- package/frontend-dist/assets/providers-DsC5Qd6m.js +1 -0
- package/frontend-dist/assets/requestDetail-C5px2Mru.js +1 -0
- package/frontend-dist/assets/requestDetail-Tozq3jwE.js +1 -0
- package/frontend-dist/assets/{sparkles-b5zac545.js → sparkles-CC-Zd8Nr.js} +1 -1
- package/frontend-dist/assets/{trash-2-BgW_R7hP.js → trash-2-D475gyZP.js} +1 -1
- package/frontend-dist/assets/{useClipboard-DWvZAkuV.js → useClipboard-B553UCM8.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-DwWdAQWh.js → useLogRetention-DvyBBmK2.js} +1 -1
- package/frontend-dist/index.html +3 -3
- package/package.json +1 -1
- package/frontend-dist/assets/Logs-BDoOY0hr.js +0 -1
- package/frontend-dist/assets/Providers-Dl1g9D6s.js +0 -1
- package/frontend-dist/assets/QuickSetup-rGmuHGJq.js +0 -1
- package/frontend-dist/assets/index-BaNw4aag.css +0 -1
- package/frontend-dist/assets/model-patches-BmLrUDyn.js +0 -1
- package/frontend-dist/assets/plus-BcoVh92Q.js +0 -1
- package/frontend-dist/assets/providers-ChuD67aW.js +0 -1
- package/frontend-dist/assets/providers-DEOmviin.js +0 -1
- package/frontend-dist/assets/requestDetail-BI1tm09T.js +0 -1
- package/frontend-dist/assets/requestDetail-dJ1P4rgQ.js +0 -1
|
@@ -94,12 +94,12 @@
|
|
|
94
94
|
"group": "智谱",
|
|
95
95
|
"presets": [
|
|
96
96
|
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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",
|
|
103
103
|
"models": [
|
|
104
104
|
"glm-5.1",
|
|
105
105
|
"glm-5",
|
|
@@ -108,6 +108,22 @@
|
|
|
108
108
|
"glm-4.5-air"
|
|
109
109
|
]
|
|
110
110
|
},
|
|
111
|
+
{
|
|
112
|
+
"plan": "Coding Plan (Anthropic)",
|
|
113
|
+
"presetName": "zhipu-coding-plan-anthropic",
|
|
114
|
+
"apiType": "anthropic",
|
|
115
|
+
"baseUrl": "https://open.bigmodel.cn",
|
|
116
|
+
"upstreamPath": "/api/anthropic/v1/messages",
|
|
117
|
+
"modelsEndpoint": "/api/anthropic/v1/models",
|
|
118
|
+
"models": [
|
|
119
|
+
"glm-5.1",
|
|
120
|
+
"glm-5",
|
|
121
|
+
"glm-5-turbo",
|
|
122
|
+
"glm-4.7",
|
|
123
|
+
"glm-4.5-air"
|
|
124
|
+
],
|
|
125
|
+
"hidden": true
|
|
126
|
+
},
|
|
111
127
|
{
|
|
112
128
|
"plan": "API",
|
|
113
129
|
"presetName": "zhipu",
|
|
@@ -352,4 +368,4 @@
|
|
|
352
368
|
}
|
|
353
369
|
]
|
|
354
370
|
}
|
|
355
|
-
]
|
|
371
|
+
]
|
package/dist/admin/providers.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, getAllMappingGroups, updateMappingGroup, PROVIDER_CONCURRENCY_DEFAULTS } from "../db/index.js";
|
|
3
|
+
import { parseEndpoints, serializeEndpoints } from "../db/providers.js";
|
|
3
4
|
import { encrypt, decrypt } from "../utils/crypto.js";
|
|
4
5
|
import { getSetting } from "../db/settings.js";
|
|
5
6
|
import { HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_BAD_REQUEST, HTTP_OK } from "./constants.js";
|
|
@@ -26,10 +27,11 @@ function cascadeProviderDisable(db, providerId) {
|
|
|
26
27
|
let modified = false;
|
|
27
28
|
let shouldDisable = false;
|
|
28
29
|
// 归一化旧格式 { default, windows } → { targets }(向后兼容 migration 026 前数据)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
if (!Array.isArray(rule.targets)) {
|
|
31
|
+
const legacyDefault = rule["default"];
|
|
32
|
+
if (typeof legacyDefault === "object" && legacyDefault !== null) {
|
|
33
|
+
rule.targets = [legacyDefault];
|
|
34
|
+
}
|
|
33
35
|
}
|
|
34
36
|
const targets = rule.targets;
|
|
35
37
|
if (Array.isArray(targets)) {
|
|
@@ -95,12 +97,42 @@ function isValidHttpUrl(str) {
|
|
|
95
97
|
return false;
|
|
96
98
|
}
|
|
97
99
|
}
|
|
100
|
+
/** 校验 endpoints 数组并加密 api_key。成功返回处理后的数组,失败返回错误信息 */
|
|
101
|
+
function validateAndEncryptEndpoints(endpoints, encryptionKey) {
|
|
102
|
+
const apiTypes = endpoints.map(e => e.api_type);
|
|
103
|
+
if (new Set(apiTypes).size !== apiTypes.length) {
|
|
104
|
+
return { ok: false, message: "endpoints 中存在重复的 api_type" };
|
|
105
|
+
}
|
|
106
|
+
for (const ep of endpoints) {
|
|
107
|
+
if (!isValidHttpUrl(ep.base_url)) {
|
|
108
|
+
return { ok: false, message: `endpoint (${ep.api_type}) base_url 格式无效,必须是以 http:// 或 https:// 开头的合法 URL` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const processed = endpoints.map(e => ({
|
|
112
|
+
api_type: e.api_type,
|
|
113
|
+
base_url: e.base_url,
|
|
114
|
+
upstream_path: e.upstream_path ?? null,
|
|
115
|
+
api_key: e.api_key ? encrypt(e.api_key, encryptionKey) : null,
|
|
116
|
+
}));
|
|
117
|
+
return { ok: true, processed };
|
|
118
|
+
}
|
|
119
|
+
const EndpointSchema = Type.Object({
|
|
120
|
+
api_type: Type.Union([
|
|
121
|
+
Type.Literal("openai"),
|
|
122
|
+
Type.Literal("openai-responses"),
|
|
123
|
+
Type.Literal("anthropic"),
|
|
124
|
+
]),
|
|
125
|
+
base_url: Type.String({ minLength: 1 }),
|
|
126
|
+
upstream_path: Type.Optional(Type.Union([Type.String({ minLength: 1 }), Type.Null()])),
|
|
127
|
+
api_key: Type.Optional(Type.Union([Type.String({ minLength: 1 }), Type.Null()])),
|
|
128
|
+
});
|
|
98
129
|
const CreateProviderSchema = Type.Object({
|
|
99
130
|
name: Type.String({ minLength: 1 }),
|
|
100
|
-
api_type: Type.Union([Type.Literal("openai"), Type.Literal("anthropic")]),
|
|
101
|
-
base_url: Type.String({ minLength: 1 }),
|
|
131
|
+
api_type: Type.Optional(Type.Union([Type.Literal("openai"), Type.Literal("anthropic"), Type.Literal("openai-responses")])),
|
|
132
|
+
base_url: Type.Optional(Type.String({ minLength: 1 })),
|
|
102
133
|
upstream_path: Type.Optional(Type.String({ minLength: 1 })),
|
|
103
|
-
api_key: Type.String({ minLength: 1 }),
|
|
134
|
+
api_key: Type.Optional(Type.String({ minLength: 1 })),
|
|
135
|
+
endpoints: Type.Optional(Type.Array(EndpointSchema, { minItems: 1 })),
|
|
104
136
|
models: Type.Optional(Type.Array(Type.Union([
|
|
105
137
|
Type.String(),
|
|
106
138
|
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())) }),
|
|
@@ -118,10 +150,11 @@ const CreateProviderSchema = Type.Object({
|
|
|
118
150
|
});
|
|
119
151
|
const UpdateProviderSchema = Type.Object({
|
|
120
152
|
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
121
|
-
api_type: Type.Optional(Type.Union([Type.Literal("openai"), Type.Literal("anthropic")])),
|
|
153
|
+
api_type: Type.Optional(Type.Union([Type.Literal("openai"), Type.Literal("anthropic"), Type.Literal("openai-responses")])),
|
|
122
154
|
base_url: Type.Optional(Type.String({ minLength: 1 })),
|
|
123
155
|
upstream_path: Type.Optional(Type.String({ minLength: 1 })),
|
|
124
156
|
api_key: Type.Optional(Type.String({ minLength: 1 })),
|
|
157
|
+
endpoints: Type.Optional(Type.Array(EndpointSchema, { minItems: 1 })),
|
|
125
158
|
models: Type.Optional(Type.Array(Type.Union([
|
|
126
159
|
Type.String(),
|
|
127
160
|
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())) }),
|
|
@@ -137,6 +170,104 @@ const UpdateProviderSchema = Type.Object({
|
|
|
137
170
|
proxy_username: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
138
171
|
proxy_password: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
139
172
|
});
|
|
173
|
+
async function handleCreateProvider(body, reply, deps) {
|
|
174
|
+
const { db, stateRegistry, tracker, adaptiveController } = deps;
|
|
175
|
+
if (!PROVIDER_NAME_RE.test(body.name)) {
|
|
176
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "Provider 名称仅允许英文大小写字母、数字、横线和下划线"));
|
|
177
|
+
}
|
|
178
|
+
const existing = db.prepare("SELECT id FROM providers WHERE name = ?").get(body.name);
|
|
179
|
+
if (existing) {
|
|
180
|
+
return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, `Provider 名称 '${body.name}' 已存在`));
|
|
181
|
+
}
|
|
182
|
+
const encryptionKey = getSetting(db, "encryption_key");
|
|
183
|
+
// ---- endpoints 解析 ----
|
|
184
|
+
let legacyApiType;
|
|
185
|
+
let legacyBaseUrl;
|
|
186
|
+
let legacyUpstreamPath;
|
|
187
|
+
let legacyApiKeyPlain;
|
|
188
|
+
let endpointsSerialized;
|
|
189
|
+
if (body.endpoints && body.endpoints.length > 0) {
|
|
190
|
+
const result = validateAndEncryptEndpoints(body.endpoints, encryptionKey);
|
|
191
|
+
if (!result.ok) {
|
|
192
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, result.message));
|
|
193
|
+
}
|
|
194
|
+
endpointsSerialized = serializeEndpoints(result.processed);
|
|
195
|
+
legacyApiType = body.endpoints[0].api_type;
|
|
196
|
+
legacyBaseUrl = body.endpoints[0].base_url;
|
|
197
|
+
legacyUpstreamPath = body.endpoints[0].upstream_path ?? null;
|
|
198
|
+
legacyApiKeyPlain = body.endpoints[0].api_key ?? body.api_key ?? "";
|
|
199
|
+
if (!legacyApiKeyPlain) {
|
|
200
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "第一个 endpoint 需要提供 api_key 或使用旧 api_key 字段"));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
if (!body.api_type || !body.base_url || !body.api_key) {
|
|
205
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "缺少 endpoints 时,api_type/base_url/api_key 为必填"));
|
|
206
|
+
}
|
|
207
|
+
if (!isValidHttpUrl(body.base_url)) {
|
|
208
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "base_url 格式无效,必须是以 http:// 或 https:// 开头的合法 URL"));
|
|
209
|
+
}
|
|
210
|
+
legacyApiType = body.api_type;
|
|
211
|
+
legacyBaseUrl = body.base_url;
|
|
212
|
+
legacyUpstreamPath = body.upstream_path ?? null;
|
|
213
|
+
legacyApiKeyPlain = body.api_key;
|
|
214
|
+
endpointsSerialized = serializeEndpoints([{
|
|
215
|
+
api_type: body.api_type,
|
|
216
|
+
base_url: body.base_url,
|
|
217
|
+
upstream_path: body.upstream_path ?? null,
|
|
218
|
+
api_key: null,
|
|
219
|
+
}]);
|
|
220
|
+
}
|
|
221
|
+
const encryptedKey = encrypt(legacyApiKeyPlain, encryptionKey);
|
|
222
|
+
const { entries: normalizedModels, overrides: contextOverrides } = extractModelOverrides((body.models ?? []));
|
|
223
|
+
const isAdaptiveEnabled = body.adaptive_enabled ?? 0;
|
|
224
|
+
const effectiveProxyType = body.proxy_url?.trim() ? body.proxy_type : null;
|
|
225
|
+
const effectiveProxyUrl = body.proxy_url?.trim() || null;
|
|
226
|
+
const encryptedProxyUsername = (effectiveProxyType && body.proxy_username) ? encrypt(body.proxy_username, encryptionKey) : null;
|
|
227
|
+
const encryptedProxyPassword = (effectiveProxyType && body.proxy_password) ? encrypt(body.proxy_password, encryptionKey) : null;
|
|
228
|
+
const id = createProvider(db, {
|
|
229
|
+
name: body.name,
|
|
230
|
+
api_type: legacyApiType,
|
|
231
|
+
base_url: legacyBaseUrl,
|
|
232
|
+
upstream_path: legacyUpstreamPath,
|
|
233
|
+
api_key: encryptedKey,
|
|
234
|
+
api_key_preview: legacyApiKeyPlain.length > API_KEY_PREVIEW_MIN_LENGTH ? `${legacyApiKeyPlain.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${legacyApiKeyPlain.slice(-API_KEY_PREVIEW_PREFIX_LEN)}` : "****",
|
|
235
|
+
models: JSON.stringify(normalizedModels),
|
|
236
|
+
is_active: body.is_active ?? 1,
|
|
237
|
+
max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
238
|
+
queue_timeout_ms: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
239
|
+
max_queue_size: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
240
|
+
adaptive_enabled: isAdaptiveEnabled,
|
|
241
|
+
proxy_type: effectiveProxyType,
|
|
242
|
+
proxy_url: effectiveProxyUrl,
|
|
243
|
+
proxy_username: encryptedProxyUsername,
|
|
244
|
+
proxy_password: encryptedProxyPassword,
|
|
245
|
+
endpoints: endpointsSerialized,
|
|
246
|
+
});
|
|
247
|
+
if (contextOverrides.length > 0) {
|
|
248
|
+
setModelInfoForProvider(db, id, contextOverrides.map(o => ({ model_name: o.name, context_window: o.context_window })));
|
|
249
|
+
}
|
|
250
|
+
if (!isAdaptiveEnabled) {
|
|
251
|
+
stateRegistry?.updateProviderConcurrency(id, {
|
|
252
|
+
maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
253
|
+
queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
254
|
+
maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
adaptiveController?.syncProvider(id, {
|
|
258
|
+
adaptive_enabled: isAdaptiveEnabled,
|
|
259
|
+
max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
260
|
+
queue_timeout_ms: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
261
|
+
max_queue_size: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
262
|
+
});
|
|
263
|
+
tracker?.updateProviderConfig(id, {
|
|
264
|
+
name: body.name,
|
|
265
|
+
maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
266
|
+
queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
267
|
+
maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
268
|
+
});
|
|
269
|
+
return reply.code(HTTP_CREATED).send({ id });
|
|
270
|
+
}
|
|
140
271
|
export const adminProviderRoutes = (app, options, done) => {
|
|
141
272
|
const { db, stateRegistry, tracker, adaptiveController, proxyAgentFactory } = options;
|
|
142
273
|
app.get("/admin/api/providers", async (_request, reply) => {
|
|
@@ -162,6 +293,12 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
162
293
|
proxy_url: s.proxy_url,
|
|
163
294
|
proxy_username: s.proxy_username ? decrypt(s.proxy_username, encryptionKey) : null,
|
|
164
295
|
proxy_password: s.proxy_password ? decrypt(s.proxy_password, encryptionKey) : null,
|
|
296
|
+
endpoints: parseEndpoints(s.endpoints).map(ep => ({
|
|
297
|
+
api_type: ep.api_type,
|
|
298
|
+
base_url: ep.base_url,
|
|
299
|
+
upstream_path: ep.upstream_path ?? null,
|
|
300
|
+
api_key: ep.api_key ? decrypt(ep.api_key, encryptionKey) : "",
|
|
301
|
+
})),
|
|
165
302
|
concurrency_status: stateRegistry?.getProviderStatus(s.id) ?? { active: 0, queued: 0 },
|
|
166
303
|
created_at: s.created_at,
|
|
167
304
|
updated_at: s.updated_at,
|
|
@@ -169,67 +306,7 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
169
306
|
}));
|
|
170
307
|
});
|
|
171
308
|
app.post("/admin/api/providers", { schema: { body: CreateProviderSchema } }, async (request, reply) => {
|
|
172
|
-
|
|
173
|
-
if (!PROVIDER_NAME_RE.test(body.name)) {
|
|
174
|
-
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "Provider 名称仅允许英文大小写字母、数字、横线和下划线"));
|
|
175
|
-
}
|
|
176
|
-
const existing = db.prepare("SELECT id FROM providers WHERE name = ?").get(body.name);
|
|
177
|
-
if (existing) {
|
|
178
|
-
return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, `Provider 名称 '${body.name}' 已存在`));
|
|
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
|
-
}
|
|
183
|
-
const encryptedKey = encrypt(body.api_key, getSetting(db, "encryption_key"));
|
|
184
|
-
const { entries: normalizedModels, overrides: contextOverrides } = extractModelOverrides((body.models ?? []));
|
|
185
|
-
const isAdaptiveEnabled = body.adaptive_enabled ?? 0;
|
|
186
|
-
// 将空 proxy_url 视为不使用代理
|
|
187
|
-
const effectiveProxyType = body.proxy_url?.trim() ? body.proxy_type : null;
|
|
188
|
-
const effectiveProxyUrl = body.proxy_url?.trim() || null;
|
|
189
|
-
const encryptedProxyUsername = (effectiveProxyType && body.proxy_username) ? encrypt(body.proxy_username, getSetting(db, "encryption_key")) : null;
|
|
190
|
-
const encryptedProxyPassword = (effectiveProxyType && body.proxy_password) ? encrypt(body.proxy_password, getSetting(db, "encryption_key")) : null;
|
|
191
|
-
const id = createProvider(db, {
|
|
192
|
-
name: body.name,
|
|
193
|
-
api_type: body.api_type,
|
|
194
|
-
base_url: body.base_url,
|
|
195
|
-
upstream_path: body.upstream_path ?? null,
|
|
196
|
-
api_key: encryptedKey,
|
|
197
|
-
api_key_preview: body.api_key.length > API_KEY_PREVIEW_MIN_LENGTH ? `${body.api_key.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${body.api_key.slice(-API_KEY_PREVIEW_PREFIX_LEN)}` : "****",
|
|
198
|
-
models: JSON.stringify(normalizedModels),
|
|
199
|
-
is_active: body.is_active ?? 1,
|
|
200
|
-
max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
201
|
-
queue_timeout_ms: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
202
|
-
max_queue_size: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
203
|
-
adaptive_enabled: isAdaptiveEnabled,
|
|
204
|
-
proxy_type: effectiveProxyType,
|
|
205
|
-
proxy_url: effectiveProxyUrl,
|
|
206
|
-
proxy_username: encryptedProxyUsername,
|
|
207
|
-
proxy_password: encryptedProxyPassword,
|
|
208
|
-
});
|
|
209
|
-
if (contextOverrides.length > 0) {
|
|
210
|
-
setModelInfoForProvider(db, id, contextOverrides.map(o => ({ model_name: o.name, context_window: o.context_window })));
|
|
211
|
-
}
|
|
212
|
-
// 当 adaptive 启用时,由 syncProvider 全权管理信号量(避免重复调用 updateConfig)
|
|
213
|
-
if (!isAdaptiveEnabled) {
|
|
214
|
-
stateRegistry?.updateProviderConcurrency(id, {
|
|
215
|
-
maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
216
|
-
queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
217
|
-
maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
adaptiveController?.syncProvider(id, {
|
|
221
|
-
adaptive_enabled: isAdaptiveEnabled,
|
|
222
|
-
max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
223
|
-
queue_timeout_ms: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
224
|
-
max_queue_size: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
225
|
-
});
|
|
226
|
-
tracker?.updateProviderConfig(id, {
|
|
227
|
-
name: body.name,
|
|
228
|
-
maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
229
|
-
queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
230
|
-
maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
231
|
-
});
|
|
232
|
-
return reply.code(HTTP_CREATED).send({ id });
|
|
309
|
+
return handleCreateProvider(request.body, reply, { db, stateRegistry, tracker, adaptiveController });
|
|
233
310
|
});
|
|
234
311
|
app.put("/admin/api/providers/:id", { schema: { body: UpdateProviderSchema } }, async (request, reply) => {
|
|
235
312
|
const { id } = request.params;
|
|
@@ -295,6 +372,24 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
295
372
|
if (body.proxy_password !== undefined && effectiveProxyType) {
|
|
296
373
|
fields.proxy_password = body.proxy_password ? encrypt(body.proxy_password, getSetting(db, "encryption_key")) : null;
|
|
297
374
|
}
|
|
375
|
+
// ---- endpoints 处理 ----
|
|
376
|
+
if (body.endpoints) {
|
|
377
|
+
const ek = getSetting(db, "encryption_key");
|
|
378
|
+
const result = validateAndEncryptEndpoints(body.endpoints, ek);
|
|
379
|
+
if (!result.ok) {
|
|
380
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, result.message));
|
|
381
|
+
}
|
|
382
|
+
fields.endpoints = serializeEndpoints(result.processed);
|
|
383
|
+
// 同步旧字段 = endpoints[0]
|
|
384
|
+
fields.api_type = body.endpoints[0].api_type;
|
|
385
|
+
fields.base_url = body.endpoints[0].base_url;
|
|
386
|
+
fields.upstream_path = body.endpoints[0].upstream_path ?? null;
|
|
387
|
+
const primaryApiKey = body.endpoints[0].api_key;
|
|
388
|
+
if (primaryApiKey) {
|
|
389
|
+
fields.api_key = encrypt(primaryApiKey, ek);
|
|
390
|
+
fields.api_key_preview = primaryApiKey.length > API_KEY_PREVIEW_MIN_LENGTH ? `${primaryApiKey.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${primaryApiKey.slice(-API_KEY_PREVIEW_PREFIX_LEN)}` : "****";
|
|
391
|
+
}
|
|
392
|
+
}
|
|
298
393
|
updateProvider(db, id, fields);
|
|
299
394
|
proxyAgentFactory?.invalidate(id);
|
|
300
395
|
const updated = getProviderById(db, id);
|
|
@@ -94,12 +94,12 @@
|
|
|
94
94
|
"group": "智谱",
|
|
95
95
|
"presets": [
|
|
96
96
|
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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",
|
|
103
103
|
"models": [
|
|
104
104
|
"glm-5.1",
|
|
105
105
|
"glm-5",
|
|
@@ -108,6 +108,22 @@
|
|
|
108
108
|
"glm-4.5-air"
|
|
109
109
|
]
|
|
110
110
|
},
|
|
111
|
+
{
|
|
112
|
+
"plan": "Coding Plan (Anthropic)",
|
|
113
|
+
"presetName": "zhipu-coding-plan-anthropic",
|
|
114
|
+
"apiType": "anthropic",
|
|
115
|
+
"baseUrl": "https://open.bigmodel.cn",
|
|
116
|
+
"upstreamPath": "/api/anthropic/v1/messages",
|
|
117
|
+
"modelsEndpoint": "/api/anthropic/v1/models",
|
|
118
|
+
"models": [
|
|
119
|
+
"glm-5.1",
|
|
120
|
+
"glm-5",
|
|
121
|
+
"glm-5-turbo",
|
|
122
|
+
"glm-4.7",
|
|
123
|
+
"glm-4.5-air"
|
|
124
|
+
],
|
|
125
|
+
"hidden": true
|
|
126
|
+
},
|
|
111
127
|
{
|
|
112
128
|
"plan": "API",
|
|
113
129
|
"presetName": "zhipu",
|
|
@@ -352,4 +368,4 @@
|
|
|
352
368
|
}
|
|
353
369
|
]
|
|
354
370
|
}
|
|
355
|
-
]
|
|
371
|
+
]
|
|
@@ -9,6 +9,8 @@ export interface ProviderPreset {
|
|
|
9
9
|
models: string[];
|
|
10
10
|
/** 由 API handler 补充:模型名 → capabilities 映射 */
|
|
11
11
|
modelCapabilities?: Record<string, string[]>;
|
|
12
|
+
/** 隐藏 preset,不在 plan 下拉菜单显示,但 endpoints 生成仍遍历 */
|
|
13
|
+
hidden?: boolean;
|
|
12
14
|
}
|
|
13
15
|
export interface ProviderGroup {
|
|
14
16
|
group: string;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface MetricsResult {
|
|
|
57
57
|
text_tokens: number | null;
|
|
58
58
|
tool_use_tokens: number | null;
|
|
59
59
|
}
|
|
60
|
+
/** Provider endpoint API 类型(openai / openai-responses / anthropic) */
|
|
61
|
+
export type ApiType = "openai" | "openai-responses" | "anthropic";
|
|
60
62
|
export type RawHeaders = Record<string, string | string[] | undefined>;
|
|
61
63
|
export type TransportResult = {
|
|
62
64
|
kind: "success";
|
|
@@ -119,5 +121,20 @@ export interface ResilienceAttempt {
|
|
|
119
121
|
/** response headers 是否已发送,影响重试/failover 决策 */
|
|
120
122
|
headers_sent?: boolean | null;
|
|
121
123
|
}
|
|
124
|
+
/** Provider endpoint — stored in providers.endpoints JSON field */
|
|
125
|
+
export interface ProviderEndpoint {
|
|
126
|
+
api_type: ApiType;
|
|
127
|
+
base_url: string;
|
|
128
|
+
upstream_path?: string | null;
|
|
129
|
+
api_key?: string | null;
|
|
130
|
+
}
|
|
131
|
+
/** resolveEndpoint() 的输出 — 所有下游消费者只消费此对象 */
|
|
132
|
+
export interface ResolvedEndpoint {
|
|
133
|
+
api_type: ApiType;
|
|
134
|
+
base_url: string;
|
|
135
|
+
upstream_path: string | null;
|
|
136
|
+
api_key: string;
|
|
137
|
+
needs_transform: boolean;
|
|
138
|
+
}
|
|
122
139
|
/** 流式传输阶段状态 */
|
|
123
140
|
export type StreamState = "BUFFERING" | "STREAMING" | "COMPLETED" | "EARLY_ERROR" | "ABORTED";
|
package/dist/db/logs.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface RequestLog {
|
|
|
21
21
|
original_model: string | null;
|
|
22
22
|
stream_text_content: string | null;
|
|
23
23
|
session_id: string | null;
|
|
24
|
+
upstream_api_type: string | null;
|
|
25
|
+
upstream_base_url: string | null;
|
|
24
26
|
}
|
|
25
27
|
/** 列表查询扩展字段:JOIN providers 获得 provider_name */
|
|
26
28
|
export interface RequestLogListRow extends RequestLog {
|
|
@@ -57,6 +59,8 @@ export interface RequestLogInsert {
|
|
|
57
59
|
resilience_reason?: string | null;
|
|
58
60
|
mapping_reason?: string | null;
|
|
59
61
|
failover_trigger?: string | null;
|
|
62
|
+
upstream_api_type?: string | null;
|
|
63
|
+
upstream_base_url?: string | null;
|
|
60
64
|
}
|
|
61
65
|
export interface LogWriteContext {
|
|
62
66
|
matcher?: RetryMatcher | null;
|
package/dist/db/logs.js
CHANGED
|
@@ -9,6 +9,7 @@ const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status
|
|
|
9
9
|
rm.tokens_per_second, rm.stop_reason, rm.backend_model, rm.is_complete AS metrics_complete,
|
|
10
10
|
rm.input_tokens_estimated, rm.client_type, rm.cache_read_tokens_estimated,
|
|
11
11
|
COALESCE(p.name, rl.provider_id) AS provider_name,
|
|
12
|
+
rl.upstream_api_type, rl.upstream_base_url,
|
|
12
13
|
CASE
|
|
13
14
|
WHEN rl.client_request IS NULL THEN 'off'
|
|
14
15
|
WHEN rl.api_type = 'anthropic' THEN COALESCE(json_extract(rl.client_request, '$.body.thinking.type'), 'off')
|
|
@@ -22,9 +23,10 @@ function rawInsertRequestLog(db, log, writeContext) {
|
|
|
22
23
|
getCachedStmt(db, `INSERT INTO request_logs (id, api_type, model, provider_id, status_code, client_status_code, latency_ms,
|
|
23
24
|
is_stream, error_message, created_at, client_request, upstream_request, upstream_response,
|
|
24
25
|
is_retry, is_failover, original_request_id, router_key_id, original_model, session_id, pipeline_snapshot,
|
|
25
|
-
transport_kind, abort_reason, error_code, headers_sent, resilience_action, resilience_reason, mapping_reason, failover_trigger
|
|
26
|
+
transport_kind, abort_reason, error_code, headers_sent, resilience_action, resilience_reason, mapping_reason, failover_trigger,
|
|
27
|
+
upstream_api_type, upstream_base_url)
|
|
26
28
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
27
|
-
?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.client_status_code ?? null, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.client_request ?? null, preserveDetail ? (log.upstream_request ?? null) : null, preserveDetail ? (log.upstream_response ?? null) : null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null, log.session_id ?? null, log.pipeline_snapshot ?? null, log.transport_kind ?? null, log.abort_reason ?? null, log.error_code ?? null, log.headers_sent ?? null, log.resilience_action ?? null, log.resilience_reason ?? null, log.mapping_reason ?? null, log.failover_trigger ?? null);
|
|
29
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.client_status_code ?? null, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.client_request ?? null, preserveDetail ? (log.upstream_request ?? null) : null, preserveDetail ? (log.upstream_response ?? null) : null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null, log.session_id ?? null, log.pipeline_snapshot ?? null, log.transport_kind ?? null, log.abort_reason ?? null, log.error_code ?? null, log.headers_sent ?? null, log.resilience_action ?? null, log.resilience_reason ?? null, log.mapping_reason ?? null, log.failover_trigger ?? null, log.upstream_api_type ?? null, log.upstream_base_url ?? null);
|
|
28
30
|
}
|
|
29
31
|
export function insertRequestLog(db, log, writeContext) {
|
|
30
32
|
// 文件写入:始终同步调用(WriteStream 内部异步,不阻塞事件循环)
|
|
@@ -110,7 +112,8 @@ export function getRequestLogById(db, id) {
|
|
|
110
112
|
return db.prepare(`SELECT rl.*, rm.input_tokens, rm.output_tokens, rm.cache_read_tokens, rm.ttft_ms,
|
|
111
113
|
rm.tokens_per_second, rm.stop_reason, rm.backend_model, rm.is_complete AS metrics_complete,
|
|
112
114
|
rm.input_tokens_estimated, rm.client_type, rm.cache_read_tokens_estimated,
|
|
113
|
-
COALESCE(p.name, rl.provider_id) AS provider_name${LOG_DETAIL_THINKING_LEVEL}
|
|
115
|
+
COALESCE(p.name, rl.provider_id) AS provider_name${LOG_DETAIL_THINKING_LEVEL},
|
|
116
|
+
rl.upstream_api_type, rl.upstream_base_url
|
|
114
117
|
FROM request_logs rl
|
|
115
118
|
LEFT JOIN providers p ON p.id = rl.provider_id
|
|
116
119
|
LEFT JOIN request_metrics rm ON rm.request_log_id = rl.id
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- 051: Add endpoints column to providers, migrate existing data to endpoints JSON
|
|
2
|
+
ALTER TABLE providers ADD COLUMN endpoints TEXT DEFAULT NULL;
|
|
3
|
+
|
|
4
|
+
UPDATE providers
|
|
5
|
+
SET endpoints = json_array(
|
|
6
|
+
json_object(
|
|
7
|
+
'api_type', api_type,
|
|
8
|
+
'base_url', base_url,
|
|
9
|
+
'upstream_path', CASE WHEN upstream_path IS NULL THEN json('null') ELSE upstream_path END,
|
|
10
|
+
'api_key', CASE WHEN api_key IS NULL THEN json('null') ELSE api_key END
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
WHERE endpoints IS NULL
|
|
14
|
+
AND api_type IS NOT NULL
|
|
15
|
+
AND base_url IS NOT NULL;
|
package/dist/db/providers.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
+
import type { ProviderEndpoint } from "../core/types.js";
|
|
2
3
|
export interface Provider {
|
|
3
4
|
id: string;
|
|
4
5
|
name: string;
|
|
@@ -18,6 +19,8 @@ export interface Provider {
|
|
|
18
19
|
proxy_url: string | null;
|
|
19
20
|
proxy_username: string | null;
|
|
20
21
|
proxy_password: string | null;
|
|
22
|
+
/** @internal 原始 JSON 文本,业务层用 parseEndpoints() 解析 */
|
|
23
|
+
endpoints?: string;
|
|
21
24
|
created_at: string;
|
|
22
25
|
updated_at: string;
|
|
23
26
|
}
|
|
@@ -30,6 +33,10 @@ export declare const PROVIDER_CONCURRENCY_DEFAULTS: {
|
|
|
30
33
|
readonly queue_timeout_ms: 0;
|
|
31
34
|
readonly max_queue_size: 100;
|
|
32
35
|
};
|
|
36
|
+
/** 解析 endpoints JSON 文本为类型安全的数组 */
|
|
37
|
+
export declare function parseEndpoints(endpointsJson: string | null | undefined): ProviderEndpoint[];
|
|
38
|
+
/** 将 endpoints 数组序列化为 JSON 文本(用于 DB 写入) */
|
|
39
|
+
export declare function serializeEndpoints(endpoints: ProviderEndpoint[]): string;
|
|
33
40
|
export declare function getActiveProviders(db: Database.Database, apiType: "openai" | "openai-responses" | "anthropic"): Provider[];
|
|
34
41
|
export declare function getAllProviders(db: Database.Database): Provider[];
|
|
35
42
|
export declare function getProviderById(db: Database.Database, id: string): Provider | undefined;
|
|
@@ -50,8 +57,9 @@ export declare function createProvider(db: Database.Database, provider: {
|
|
|
50
57
|
proxy_url?: string | null;
|
|
51
58
|
proxy_username?: string | null;
|
|
52
59
|
proxy_password?: string | null;
|
|
60
|
+
endpoints?: string;
|
|
53
61
|
}): string;
|
|
54
|
-
export declare function updateProvider(db: Database.Database, id: string, fields: Partial<Pick<Provider, "name" | "api_type" | "base_url" | "upstream_path" | "api_key" | "api_key_preview" | "models" | "is_active" | "max_concurrency" | "queue_timeout_ms" | "max_queue_size" | "adaptive_enabled" | "proxy_type" | "proxy_url" | "proxy_username" | "proxy_password">>): void;
|
|
62
|
+
export declare function updateProvider(db: Database.Database, id: string, fields: Partial<Pick<Provider, "name" | "api_type" | "base_url" | "upstream_path" | "api_key" | "api_key_preview" | "models" | "is_active" | "max_concurrency" | "queue_timeout_ms" | "max_queue_size" | "adaptive_enabled" | "proxy_type" | "proxy_url" | "proxy_username" | "proxy_password" | "endpoints">>): void;
|
|
55
63
|
export declare function deleteProvider(db: Database.Database, id: string): void;
|
|
56
64
|
export declare function getActiveProviderByName(db: Database.Database, name: string): {
|
|
57
65
|
id: string;
|
package/dist/db/providers.js
CHANGED
|
@@ -22,8 +22,37 @@ export const PROVIDER_CONCURRENCY_DEFAULTS = {
|
|
|
22
22
|
max_queue_size: 100,
|
|
23
23
|
};
|
|
24
24
|
const PROVIDER_FIELDS = new Set([
|
|
25
|
-
"name", "api_type", "base_url", "upstream_path", "api_key", "api_key_preview", "models", "is_active", "max_concurrency", "queue_timeout_ms", "max_queue_size", "adaptive_enabled", "proxy_type", "proxy_url", "proxy_username", "proxy_password",
|
|
25
|
+
"name", "api_type", "base_url", "upstream_path", "api_key", "api_key_preview", "models", "is_active", "max_concurrency", "queue_timeout_ms", "max_queue_size", "adaptive_enabled", "proxy_type", "proxy_url", "proxy_username", "proxy_password", "endpoints",
|
|
26
26
|
]);
|
|
27
|
+
const VALID_API_TYPES = new Set(["openai", "openai-responses", "anthropic"]);
|
|
28
|
+
/** 解析 endpoints JSON 文本为类型安全的数组 */
|
|
29
|
+
export function parseEndpoints(endpointsJson) {
|
|
30
|
+
if (!endpointsJson)
|
|
31
|
+
return [];
|
|
32
|
+
const parsed = JSON.parse(endpointsJson);
|
|
33
|
+
if (!Array.isArray(parsed)) {
|
|
34
|
+
throw new Error(`Invalid endpoints JSON: not an array`);
|
|
35
|
+
}
|
|
36
|
+
// Validate every element is a non-null object with required fields
|
|
37
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
38
|
+
const item = parsed[i];
|
|
39
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
40
|
+
throw new Error(`Invalid endpoints JSON: element [${i}] is not an object`);
|
|
41
|
+
}
|
|
42
|
+
const obj = item;
|
|
43
|
+
if (typeof obj.api_type !== "string" || !VALID_API_TYPES.has(obj.api_type)) {
|
|
44
|
+
throw new Error(`Invalid endpoints JSON: element [${i}] has invalid api_type '${typeof obj.api_type === "string" ? obj.api_type : JSON.stringify(obj.api_type)}', must be one of: openai, openai-responses, anthropic`);
|
|
45
|
+
}
|
|
46
|
+
if (typeof obj.base_url !== "string" || obj.base_url.trim() === "") {
|
|
47
|
+
throw new Error(`Invalid endpoints JSON: element [${i}] has invalid base_url, must be a non-empty string`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
/** 将 endpoints 数组序列化为 JSON 文本(用于 DB 写入) */
|
|
53
|
+
export function serializeEndpoints(endpoints) {
|
|
54
|
+
return JSON.stringify(endpoints);
|
|
55
|
+
}
|
|
27
56
|
export function getActiveProviders(db, apiType) {
|
|
28
57
|
return db
|
|
29
58
|
.prepare("SELECT * FROM providers WHERE api_type = ? AND is_active = 1")
|
|
@@ -38,8 +67,8 @@ export function getProviderById(db, id) {
|
|
|
38
67
|
export function createProvider(db, provider) {
|
|
39
68
|
const id = randomUUID();
|
|
40
69
|
const now = new Date().toISOString();
|
|
41
|
-
db.prepare(`INSERT INTO providers (id, name, api_type, base_url, upstream_path, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled, proxy_type, proxy_url, proxy_username, proxy_password, created_at, updated_at)
|
|
42
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, provider.name, provider.api_type, provider.base_url, provider.upstream_path ?? null, provider.api_key, provider.api_key_preview ?? null, provider.models ?? "[]", provider.is_active ?? 1, provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency, provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms, provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size, provider.adaptive_enabled ?? 0, provider.proxy_type ?? null, provider.proxy_url ?? null, provider.proxy_username ?? null, provider.proxy_password ?? null, now, now);
|
|
70
|
+
db.prepare(`INSERT INTO providers (id, name, api_type, base_url, upstream_path, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled, proxy_type, proxy_url, proxy_username, proxy_password, endpoints, created_at, updated_at)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, provider.name, provider.api_type, provider.base_url, provider.upstream_path ?? null, provider.api_key, provider.api_key_preview ?? null, provider.models ?? "[]", provider.is_active ?? 1, provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency, provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms, provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size, provider.adaptive_enabled ?? 0, provider.proxy_type ?? null, provider.proxy_url ?? null, provider.proxy_username ?? null, provider.proxy_password ?? null, provider.endpoints ?? null, now, now);
|
|
43
72
|
return id;
|
|
44
73
|
}
|
|
45
74
|
export function updateProvider(db, id, fields) {
|
package/dist/index.js
CHANGED
|
@@ -408,6 +408,19 @@ export async function main() {
|
|
|
408
408
|
const config = getConfig();
|
|
409
409
|
// 全局兜底:防止未捕获异常导致进程崩溃
|
|
410
410
|
process.on("uncaughtException", (err) => {
|
|
411
|
+
const code = err.code;
|
|
412
|
+
// EPIPE/ECONNRESET 是客户端断连后的正常网络错误,不影响服务稳定性
|
|
413
|
+
if (code === "EPIPE" || code === "ECONNRESET") {
|
|
414
|
+
try {
|
|
415
|
+
app.log.warn({ err }, "Client disconnected (EPIPE/ECONNRESET)");
|
|
416
|
+
/* eslint-disable taste/no-silent-catch -- app.log 可能已崩溃 */
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
console.warn("Client disconnected:", err.message);
|
|
420
|
+
}
|
|
421
|
+
/* eslint-enable taste/no-silent-catch */
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
411
424
|
try {
|
|
412
425
|
app.log.fatal({ err }, "Uncaught exception");
|
|
413
426
|
/* eslint-disable taste/no-silent-catch -- app.log 可能已崩溃,console 是最后手段 */
|