dsh-router-core 0.3.8 → 0.3.10
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/README.md +8 -11
- package/lib/client-registry.js +71 -30
- package/lib/client.js +71 -30
- package/lib/index.js +107 -22
- package/lib/suppliers/nvidia.js +6 -1
- package/lib/suppliers/opencode.js +6 -1
- package/lib/suppliers/openrouter.js +6 -1
- package/lib/types/keys.d.ts +1 -1
- package/lib/types/llm/adapter.d.ts +2 -5
- package/lib/types/router/types.d.ts +3 -1
- package/lib/types/shared.d.ts +0 -2
- package/lib/types/supplier-config.d.ts +6 -0
- package/lib/types/suppliers/contract.d.ts +5 -1
- package/package.json +15 -19
- package/src/client/SupplierDetail.tsx +55 -2
- package/src/client/env.d.ts +7 -0
- package/src/client/router.css +14 -0
- package/src/index.ts +27 -7
- package/src/keys.ts +3 -6
- package/src/llm/adapter.test.ts +17 -0
- package/src/llm/adapter.ts +30 -4
- package/src/router/index.ts +2 -2
- package/src/router/models-cache.test.ts +1 -1
- package/src/router/precise-dispatch.test.ts +2 -2
- package/src/router/types.ts +3 -1
- package/src/router/usage.test.ts +2 -2
- package/src/shared.ts +0 -3
- package/src/supplier-config.test.ts +15 -0
- package/src/supplier-config.ts +37 -1
- package/src/suppliers/contract.ts +5 -1
- package/src/suppliers/loader.ts +7 -2
- package/src/suppliers/nvidia/plugin.ts +6 -1
- package/src/suppliers/opencode/plugin.ts +6 -1
- package/src/suppliers/openrouter/plugin.ts +6 -1
package/lib/index.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { basename, dirname, join } from "node:path";
|
|
2
2
|
import { mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders } from "@deepseek-ai/dsh-llm";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
7
7
|
//#region src/shared.ts
|
|
8
8
|
/** Same-origin API base the client panel fetches (host half serves it). */
|
|
9
9
|
const ROUTER_API_BASE = "/router/api";
|
|
10
|
-
/** Bearer key env / credential reference protecting /v1/*. */
|
|
11
|
-
const API_KEY_REF = "TW2A_API_KEY";
|
|
12
10
|
//#endregion
|
|
13
11
|
//#region src/router/account-pool.ts
|
|
14
12
|
const MINUTE = 6e4;
|
|
@@ -258,6 +256,13 @@ function readCredits(raw) {
|
|
|
258
256
|
for (const [uid, v] of Object.entries(raw)) if (typeof v === "number" && Number.isFinite(v) && v >= 0) out[uid] = v;
|
|
259
257
|
return out;
|
|
260
258
|
}
|
|
259
|
+
/** 读盘时的连接显示别名校验:只留非空字符串。 */
|
|
260
|
+
function readNames(raw) {
|
|
261
|
+
const out = {};
|
|
262
|
+
if (typeof raw !== "object" || raw === null) return out;
|
|
263
|
+
for (const [uid, v] of Object.entries(raw)) if (typeof v === "string" && v.trim() !== "") out[uid] = v.trim();
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
261
266
|
/** 通用供应商配置存储。 */
|
|
262
267
|
var SupplierConfigStore = class {
|
|
263
268
|
fp = "";
|
|
@@ -288,7 +293,8 @@ var SupplierConfigStore = class {
|
|
|
288
293
|
custom: [],
|
|
289
294
|
poolOrder: [],
|
|
290
295
|
poolStrategy: "fallback",
|
|
291
|
-
credits: {}
|
|
296
|
+
credits: {},
|
|
297
|
+
accountNames: {}
|
|
292
298
|
};
|
|
293
299
|
this.bySupplier.set(supplierId, cfg);
|
|
294
300
|
}
|
|
@@ -340,6 +346,24 @@ var SupplierConfigStore = class {
|
|
|
340
346
|
this.get(supplierId).poolStrategy = strategy;
|
|
341
347
|
this.saveLocked();
|
|
342
348
|
}
|
|
349
|
+
/** 读某连接的显示别名;没设或为空返回 `undefined`(用供应商原始昵称)。 */
|
|
350
|
+
getAccountName(supplierId, uid) {
|
|
351
|
+
const n = this.get(supplierId).accountNames[uid];
|
|
352
|
+
return typeof n === "string" && n !== "" ? n : void 0;
|
|
353
|
+
}
|
|
354
|
+
/** 设置连接显示名。空名 = 删除别名(回到供应商原始昵称)。 */
|
|
355
|
+
setAccountName(supplierId, uid, name) {
|
|
356
|
+
const cfg = this.get(supplierId);
|
|
357
|
+
const clean = (name ?? "").trim();
|
|
358
|
+
if (clean === "") {
|
|
359
|
+
if (cfg.accountNames[uid] === void 0) return;
|
|
360
|
+
delete cfg.accountNames[uid];
|
|
361
|
+
this.saveLocked();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
cfg.accountNames[uid] = clean;
|
|
365
|
+
this.saveLocked();
|
|
366
|
+
}
|
|
343
367
|
/**
|
|
344
368
|
* 读缓存的积分。没缓存过返回 `CREDITS_UNKNOWN`(-1),让调用方自己决定
|
|
345
369
|
* 显示什么——不要把 -1 当 0 展示。
|
|
@@ -380,7 +404,8 @@ var SupplierConfigStore = class {
|
|
|
380
404
|
custom: Array.isArray(c.custom) ? c.custom.filter((m) => typeof m === "string") : [],
|
|
381
405
|
poolOrder: Array.isArray(c.poolOrder) ? c.poolOrder.filter((u) => typeof u === "string") : [],
|
|
382
406
|
poolStrategy: c.poolStrategy === "round-robin" ? "round-robin" : "fallback",
|
|
383
|
-
credits: readCredits(c.credits)
|
|
407
|
+
credits: readCredits(c.credits),
|
|
408
|
+
accountNames: readNames(c.accountNames)
|
|
384
409
|
});
|
|
385
410
|
}
|
|
386
411
|
saveLocked() {
|
|
@@ -392,7 +417,8 @@ var SupplierConfigStore = class {
|
|
|
392
417
|
custom: [...cfg.custom],
|
|
393
418
|
poolOrder: [...cfg.poolOrder],
|
|
394
419
|
poolStrategy: cfg.poolStrategy,
|
|
395
|
-
credits: { ...cfg.credits }
|
|
420
|
+
credits: { ...cfg.credits },
|
|
421
|
+
accountNames: { ...cfg.accountNames }
|
|
396
422
|
};
|
|
397
423
|
try {
|
|
398
424
|
const dir = dirname(this.fp);
|
|
@@ -1921,7 +1947,7 @@ var Router = class {
|
|
|
1921
1947
|
const pool = s.pool;
|
|
1922
1948
|
const cfg = this.store.get(s.id);
|
|
1923
1949
|
if (s.accounts !== void 0 && s.accounts().length === 0) {
|
|
1924
|
-
const r = await s.chatOnce("", req);
|
|
1950
|
+
const r = await s.chatOnce("", req.lv ?? "auto", req);
|
|
1925
1951
|
if (!r.ok) {
|
|
1926
1952
|
if (r.state === "no_such_model") return false;
|
|
1927
1953
|
if (trace !== void 0) trace.lastState = r.state;
|
|
@@ -1941,7 +1967,7 @@ var Router = class {
|
|
|
1941
1967
|
const uid = pool.pick(s.accounts().filter((a) => !tried.has(a.uid)), cfg.poolOrder, cfg.poolStrategy, req.model);
|
|
1942
1968
|
if (uid === void 0) return false;
|
|
1943
1969
|
tried.add(uid);
|
|
1944
|
-
const r = await s.chatOnce(uid, req);
|
|
1970
|
+
const r = await s.chatOnce(uid, req.lv ?? "auto", req);
|
|
1945
1971
|
if (!r.ok) {
|
|
1946
1972
|
if (r.state === "no_such_model") {
|
|
1947
1973
|
if (trace !== void 0) trace.lastError = "no_such_model";
|
|
@@ -2176,7 +2202,7 @@ async function* translateSse(payloads) {
|
|
|
2176
2202
|
index: block.index,
|
|
2177
2203
|
block: {
|
|
2178
2204
|
type: "tool-call",
|
|
2179
|
-
id:
|
|
2205
|
+
id: ToolCallId(block.callId ?? ""),
|
|
2180
2206
|
name: "",
|
|
2181
2207
|
arguments: "{}"
|
|
2182
2208
|
}
|
|
@@ -2272,7 +2298,7 @@ async function* translateSse(payloads) {
|
|
|
2272
2298
|
yield {
|
|
2273
2299
|
type: "tool-call-delta",
|
|
2274
2300
|
index: block.index,
|
|
2275
|
-
id:
|
|
2301
|
+
id: ToolCallId(block.callId ?? ""),
|
|
2276
2302
|
...block.name !== void 0 ? { name: block.name } : {},
|
|
2277
2303
|
argumentsDelta: fragment
|
|
2278
2304
|
};
|
|
@@ -2318,7 +2344,7 @@ function closeBlock(block) {
|
|
|
2318
2344
|
};
|
|
2319
2345
|
case "tool-call": return {
|
|
2320
2346
|
type: "tool-call",
|
|
2321
|
-
id:
|
|
2347
|
+
id: ToolCallId(block.callId ?? ""),
|
|
2322
2348
|
name: block.name ?? "",
|
|
2323
2349
|
arguments: block.text === "" ? "{}" : block.text
|
|
2324
2350
|
};
|
|
@@ -2344,6 +2370,25 @@ function completeJson(text) {
|
|
|
2344
2370
|
* DSH adapter:provider `router`。模型目录 = 组合;stream 转发到本插件
|
|
2345
2371
|
* /v1/chat/completions(组合路由在 /v1 内完成)。
|
|
2346
2372
|
*/
|
|
2373
|
+
/** Router provider 对外暴露的推理等级(对齐 dsh/DeepSeek:off/low/high/max)。 */
|
|
2374
|
+
const ROUTER_REASONING_EFFORTS = [
|
|
2375
|
+
{
|
|
2376
|
+
id: ReasoningEffortId("off"),
|
|
2377
|
+
name: "Off"
|
|
2378
|
+
},
|
|
2379
|
+
{
|
|
2380
|
+
id: ReasoningEffortId("low"),
|
|
2381
|
+
name: "Low"
|
|
2382
|
+
},
|
|
2383
|
+
{
|
|
2384
|
+
id: ReasoningEffortId("high"),
|
|
2385
|
+
name: "High"
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
id: ReasoningEffortId("max"),
|
|
2389
|
+
name: "Max"
|
|
2390
|
+
}
|
|
2391
|
+
];
|
|
2347
2392
|
var RouterAdapter = class extends LlmAdapter {
|
|
2348
2393
|
baseURL;
|
|
2349
2394
|
source;
|
|
@@ -2373,6 +2418,18 @@ var RouterAdapter = class extends LlmAdapter {
|
|
|
2373
2418
|
}
|
|
2374
2419
|
return out;
|
|
2375
2420
|
}
|
|
2421
|
+
async resolveModel(_provider, model) {
|
|
2422
|
+
const found = (await this.source.comboModels()).find((m) => m.id === model || m.name === model);
|
|
2423
|
+
return {
|
|
2424
|
+
provider: "router",
|
|
2425
|
+
id: model,
|
|
2426
|
+
name: found?.name ?? found?.id ?? model,
|
|
2427
|
+
reasoning: {
|
|
2428
|
+
efforts: ROUTER_REASONING_EFFORTS,
|
|
2429
|
+
defaultEffort: ReasoningEffortId("high")
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2376
2433
|
async *stream(options) {
|
|
2377
2434
|
const body = wireRequest(options);
|
|
2378
2435
|
const controller = new AbortController();
|
|
@@ -2478,7 +2535,7 @@ async function* ssePayloads(body) {
|
|
|
2478
2535
|
*
|
|
2479
2536
|
* /v1/* 鉴权规则:
|
|
2480
2537
|
* - requireApiKey=false(默认)→ 不鉴权
|
|
2481
|
-
* - requireApiKey=true → Bearer 必须是「启用的库内 key
|
|
2538
|
+
* - requireApiKey=true → Bearer 必须是「启用的库内 key」
|
|
2482
2539
|
*/
|
|
2483
2540
|
function newId() {
|
|
2484
2541
|
return randomBytes(6).toString("hex");
|
|
@@ -2575,13 +2632,11 @@ var KeysStore = class {
|
|
|
2575
2632
|
this.require = v;
|
|
2576
2633
|
this.save();
|
|
2577
2634
|
}
|
|
2578
|
-
/** 校验 Bearer key:requireApiKey 关 → 放行;开 →
|
|
2635
|
+
/** 校验 Bearer key:requireApiKey 关 → 放行;开 → 必须是库内启用的 key。 */
|
|
2579
2636
|
verify(bearer) {
|
|
2580
2637
|
if (!this.require) return true;
|
|
2581
2638
|
if (bearer === void 0 || bearer === "") return false;
|
|
2582
|
-
|
|
2583
|
-
const envKey = process.env[API_KEY_REF];
|
|
2584
|
-
return envKey !== void 0 && envKey !== "" && envKey === bearer;
|
|
2639
|
+
return this.keys.some((k) => k.isActive && k.key === bearer);
|
|
2585
2640
|
}
|
|
2586
2641
|
};
|
|
2587
2642
|
//#endregion
|
|
@@ -2667,10 +2722,17 @@ function wrapModule(instance, env, source, kind = "external") {
|
|
|
2667
2722
|
icon: instance.icon,
|
|
2668
2723
|
status: () => {
|
|
2669
2724
|
const now = instance.status();
|
|
2725
|
+
const accounts = now.accounts.map((a) => {
|
|
2726
|
+
const alias = env.store.getAccountName(instance.id, a.uid);
|
|
2727
|
+
return alias === void 0 || alias === a.nickname ? a : {
|
|
2728
|
+
...a,
|
|
2729
|
+
nickname: alias
|
|
2730
|
+
};
|
|
2731
|
+
});
|
|
2670
2732
|
return {
|
|
2671
2733
|
id: now.id,
|
|
2672
2734
|
name: now.name,
|
|
2673
|
-
accounts: pool.decorate(hydrateCredits(instance.id, env.store,
|
|
2735
|
+
accounts: pool.decorate(hydrateCredits(instance.id, env.store, accounts))
|
|
2674
2736
|
};
|
|
2675
2737
|
},
|
|
2676
2738
|
listModels: (force) => instance.listModels(force),
|
|
@@ -2687,7 +2749,7 @@ function wrapModule(instance, env, source, kind = "external") {
|
|
|
2687
2749
|
getAlias: () => env.store.get(instance.id).alias || instance.id,
|
|
2688
2750
|
customModelIds: () => [...env.store.get(instance.id).custom],
|
|
2689
2751
|
accounts: () => instance.status().accounts,
|
|
2690
|
-
chatOnce: (uid, req) => instance.chatOnce(uid, req),
|
|
2752
|
+
chatOnce: (uid, lv, req) => instance.chatOnce(uid, lv, req),
|
|
2691
2753
|
removeLink: isFn(instance.removeLink) ? (uid) => instance.removeLink(uid) : void 0,
|
|
2692
2754
|
dispose: () => instance.dispose(),
|
|
2693
2755
|
pool
|
|
@@ -3220,9 +3282,9 @@ function readBody(req, limit) {
|
|
|
3220
3282
|
function apply(rawContext) {
|
|
3221
3283
|
const ctx = rawContext;
|
|
3222
3284
|
const log = (msg) => ctx.logger.info(`[dsh-router] ${msg}`);
|
|
3223
|
-
const stateFile =
|
|
3285
|
+
const stateFile = "data/state.json";
|
|
3224
3286
|
const store = new SupplierConfigStore(stateFile);
|
|
3225
|
-
const credentials = new CredentialStore(
|
|
3287
|
+
const credentials = new CredentialStore(join(dirname(stateFile), "auths"));
|
|
3226
3288
|
const router = new Router(stateFile, store, log);
|
|
3227
3289
|
const keys = new KeysStore(stateFile);
|
|
3228
3290
|
const disposers = [];
|
|
@@ -3300,7 +3362,7 @@ function apply(rawContext) {
|
|
|
3300
3362
|
});
|
|
3301
3363
|
(async () => {
|
|
3302
3364
|
try {
|
|
3303
|
-
const userDir = join(
|
|
3365
|
+
const userDir = join(join(process.env.HOME ?? "", ".dsh", "profiles", "web"), "suppliers");
|
|
3304
3366
|
const { suppliers, errors } = await loadSuppliers({
|
|
3305
3367
|
builtinDir: join(import.meta.dirname, "suppliers"),
|
|
3306
3368
|
userDir,
|
|
@@ -3360,10 +3422,12 @@ function apply(rawContext) {
|
|
|
3360
3422
|
} });
|
|
3361
3423
|
return;
|
|
3362
3424
|
}
|
|
3425
|
+
const lv = typeof peek.reasoning_effort === "string" && peek.reasoning_effort !== "" ? peek.reasoning_effort : "auto";
|
|
3363
3426
|
await router.chatCompletions({
|
|
3364
3427
|
rawBody: body,
|
|
3365
3428
|
stream: !!peek.stream,
|
|
3366
|
-
model: typeof peek.model === "string" ? peek.model : ""
|
|
3429
|
+
model: typeof peek.model === "string" ? peek.model : "",
|
|
3430
|
+
lv
|
|
3367
3431
|
}, res);
|
|
3368
3432
|
}));
|
|
3369
3433
|
route(`${ROUTER_API_BASE}/health`, (_req, res) => {
|
|
@@ -3392,6 +3456,27 @@ function apply(rawContext) {
|
|
|
3392
3456
|
})))
|
|
3393
3457
|
});
|
|
3394
3458
|
});
|
|
3459
|
+
route(`${ROUTER_API_BASE}/status/rename`, async (req, res) => {
|
|
3460
|
+
let body;
|
|
3461
|
+
try {
|
|
3462
|
+
body = JSON.parse(await readBody(req, 65536));
|
|
3463
|
+
} catch {
|
|
3464
|
+
writeJson(res, 400, {
|
|
3465
|
+
ok: false,
|
|
3466
|
+
error: "invalid JSON body"
|
|
3467
|
+
});
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
if (typeof body.supplier !== "string" || body.supplier === "" || typeof body.uid !== "string" || body.uid === "") {
|
|
3471
|
+
writeJson(res, 400, {
|
|
3472
|
+
ok: false,
|
|
3473
|
+
error: "supplier and uid are required"
|
|
3474
|
+
});
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
store.setAccountName(body.supplier, body.uid, typeof body.name === "string" ? body.name : "");
|
|
3478
|
+
writeJson(res, 200, { ok: true });
|
|
3479
|
+
});
|
|
3395
3480
|
route(`${ROUTER_API_BASE}/models`, async (_req, res) => {
|
|
3396
3481
|
try {
|
|
3397
3482
|
writeJson(res, 200, {
|
package/lib/suppliers/nvidia.js
CHANGED
|
@@ -260,7 +260,7 @@ function factory(env) {
|
|
|
260
260
|
return true;
|
|
261
261
|
},
|
|
262
262
|
/** 对单个 key 调一次上游。选号/冷却/换号是核心的活,这里只报结果。 */
|
|
263
|
-
async chatOnce(uid, req) {
|
|
263
|
+
async chatOnce(uid, lv, req) {
|
|
264
264
|
const base = stripAlias(req.model, currentAlias());
|
|
265
265
|
if (!(await allModels(false)).some((m) => m.id === base)) return {
|
|
266
266
|
ok: false,
|
|
@@ -277,6 +277,11 @@ function factory(env) {
|
|
|
277
277
|
try {
|
|
278
278
|
const obj = JSON.parse(body);
|
|
279
279
|
obj.model = base;
|
|
280
|
+
if (lv !== "auto" && lv !== "" && lv !== "none" && lv !== "off") obj.reasoning_effort = lv;
|
|
281
|
+
else if (lv === "none" || lv === "off") {
|
|
282
|
+
delete obj.reasoning_effort;
|
|
283
|
+
delete obj.reasoning_summary;
|
|
284
|
+
}
|
|
280
285
|
body = JSON.stringify(obj);
|
|
281
286
|
} catch {}
|
|
282
287
|
let upstream;
|
|
@@ -64,7 +64,7 @@ function factory(env) {
|
|
|
64
64
|
}),
|
|
65
65
|
listModels: (force) => allModels(!!force),
|
|
66
66
|
/** 无账号供应商:uid 恒为空,只认 free 模型。 */
|
|
67
|
-
async chatOnce(_uid, req) {
|
|
67
|
+
async chatOnce(_uid, lv, req) {
|
|
68
68
|
if (!isFreeModel(req.model)) return {
|
|
69
69
|
ok: false,
|
|
70
70
|
state: "no_such_model",
|
|
@@ -74,6 +74,11 @@ function factory(env) {
|
|
|
74
74
|
try {
|
|
75
75
|
const obj = JSON.parse(body);
|
|
76
76
|
obj.model = stripAlias(String(obj.model ?? req.model), currentAlias());
|
|
77
|
+
if (lv !== "auto" && lv !== "" && lv !== "none" && lv !== "off") obj.reasoning_effort = lv;
|
|
78
|
+
else if (lv === "none" || lv === "off") {
|
|
79
|
+
delete obj.reasoning_effort;
|
|
80
|
+
delete obj.reasoning_summary;
|
|
81
|
+
}
|
|
77
82
|
body = JSON.stringify(obj);
|
|
78
83
|
} catch {}
|
|
79
84
|
let upstream;
|
|
@@ -142,7 +142,7 @@ function factory(env) {
|
|
|
142
142
|
return true;
|
|
143
143
|
},
|
|
144
144
|
/** 对单个 key 调一次上游。选号/冷却/换号是核心的活,这里只报结果。 */
|
|
145
|
-
async chatOnce(uid, req) {
|
|
145
|
+
async chatOnce(uid, lv, req) {
|
|
146
146
|
const base = stripAlias(req.model, currentAlias());
|
|
147
147
|
if (!freeIds.has(base) && !(await allModels(false)).some((m) => m.id === base)) return {
|
|
148
148
|
ok: false,
|
|
@@ -159,6 +159,11 @@ function factory(env) {
|
|
|
159
159
|
try {
|
|
160
160
|
const obj = JSON.parse(body);
|
|
161
161
|
obj.model = base;
|
|
162
|
+
if (lv !== "auto" && lv !== "" && lv !== "none" && lv !== "off") obj.reasoning_effort = lv;
|
|
163
|
+
else if (lv === "none" || lv === "off") {
|
|
164
|
+
delete obj.reasoning_effort;
|
|
165
|
+
delete obj.reasoning_summary;
|
|
166
|
+
}
|
|
162
167
|
body = JSON.stringify(obj);
|
|
163
168
|
} catch {}
|
|
164
169
|
let upstream;
|
package/lib/types/keys.d.ts
CHANGED
|
@@ -34,6 +34,6 @@ export declare class KeysStore {
|
|
|
34
34
|
/** requireApiKey 开关。 */
|
|
35
35
|
get requireApiKey(): boolean;
|
|
36
36
|
set requireApiKey(v: boolean);
|
|
37
|
-
/** 校验 Bearer key:requireApiKey 关 → 放行;开 →
|
|
37
|
+
/** 校验 Bearer key:requireApiKey 关 → 放行;开 → 必须是库内启用的 key。 */
|
|
38
38
|
verify(bearer: string | undefined): boolean;
|
|
39
39
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 纯文本流:组合为文本模型,本 adapter 不处理图片附件。
|
|
8
8
|
*/
|
|
9
|
-
import { LlmAdapter, type GenerateOptions, type LlmModelInfo, type StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
9
|
+
import { LlmAdapter, type GenerateOptions, type LlmModelInfo, type LlmResolvedModelInfo, type StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
10
10
|
/** 模型目录来源:组合。 */
|
|
11
11
|
export interface RouterAdapterSource {
|
|
12
12
|
comboModels: () => Promise<Array<{
|
|
@@ -22,10 +22,6 @@ export interface RouterAdapterSource {
|
|
|
22
22
|
* 用 `for await` 是因为它天然兼容两者 —— 不必为流式再写一份。
|
|
23
23
|
*/
|
|
24
24
|
export declare function translateSse(payloads: AsyncIterable<string> | Iterable<string>): AsyncGenerator<StreamChunk>;
|
|
25
|
-
/**
|
|
26
|
-
* DSH adapter:provider `router`。模型目录 = 组合;stream 转发到本插件
|
|
27
|
-
* /v1/chat/completions(组合路由在 /v1 内完成)。
|
|
28
|
-
*/
|
|
29
25
|
export declare class RouterAdapter extends LlmAdapter {
|
|
30
26
|
private readonly baseURL;
|
|
31
27
|
private readonly source;
|
|
@@ -35,5 +31,6 @@ export declare class RouterAdapter extends LlmAdapter {
|
|
|
35
31
|
name: string;
|
|
36
32
|
};
|
|
37
33
|
listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
|
|
34
|
+
resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
38
35
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
39
36
|
}
|
|
@@ -48,6 +48,8 @@ export interface ChatRequest {
|
|
|
48
48
|
rawBody: string;
|
|
49
49
|
stream: boolean;
|
|
50
50
|
model: string;
|
|
51
|
+
/** 推理等级(reasoning effort),供应商各自决定怎么映射到上游。缺省 = 'auto'。 */
|
|
52
|
+
lv?: string;
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
55
|
* 供应商接口。核心负责账号池(选号/冷却/禁用/错误累计/遍历回退),
|
|
@@ -71,7 +73,7 @@ export interface Supplier {
|
|
|
71
73
|
/** 账号「现在状态」(从 status() 派生,插件只报它观察到的)。空 = 无账号供应商。 */
|
|
72
74
|
accounts(): SupplierAccountNow[];
|
|
73
75
|
/** 对单个账号调一次上游。不遍历账号、不管冷却、不写 res。 */
|
|
74
|
-
chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult>;
|
|
76
|
+
chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult>;
|
|
75
77
|
/** 删除链接(清理凭证;内部能力,js 契约不要求实现)。 */
|
|
76
78
|
removeLink?(uid: string): Promise<boolean>;
|
|
77
79
|
dispose(): void;
|
package/lib/types/shared.d.ts
CHANGED
|
@@ -14,8 +14,6 @@ export declare const ROUTER_ACTIVE_ATTR = "data-dsh-router-active";
|
|
|
14
14
|
export declare const ROUTER_TITLE = "\u8DEF\u7531";
|
|
15
15
|
/** Same-origin API base the client panel fetches (host half serves it). */
|
|
16
16
|
export declare const ROUTER_API_BASE = "/router/api";
|
|
17
|
-
/** Bearer key env / credential reference protecting /v1/*. */
|
|
18
|
-
export declare const API_KEY_REF = "TW2A_API_KEY";
|
|
19
17
|
/** One account row from `/router/api/status`. */
|
|
20
18
|
export interface RouterAccount {
|
|
21
19
|
uid: string;
|
|
@@ -16,6 +16,8 @@ export interface SupplierConfig {
|
|
|
16
16
|
poolStrategy: PoolStrategy;
|
|
17
17
|
/** 积分缓存:uid → 剩余额度(最后一次从插件拿到的非 -1 值)。 */
|
|
18
18
|
credits: Record<string, number>;
|
|
19
|
+
/** 每连接显示别名:uid → 显示名(空/缺省用供应商原始昵称,纯展示不改路由键)。 */
|
|
20
|
+
accountNames: Record<string, string>;
|
|
19
21
|
}
|
|
20
22
|
/** 通用供应商配置存储。 */
|
|
21
23
|
export declare class SupplierConfigStore {
|
|
@@ -45,6 +47,10 @@ export declare class SupplierConfigStore {
|
|
|
45
47
|
removeCustomModel(supplierId: string, modelId: string): void;
|
|
46
48
|
setPoolOrder(supplierId: string, uids: string[]): void;
|
|
47
49
|
setPoolStrategy(supplierId: string, strategy: string): void;
|
|
50
|
+
/** 读某连接的显示别名;没设或为空返回 `undefined`(用供应商原始昵称)。 */
|
|
51
|
+
getAccountName(supplierId: string, uid: string): string | undefined;
|
|
52
|
+
/** 设置连接显示名。空名 = 删除别名(回到供应商原始昵称)。 */
|
|
53
|
+
setAccountName(supplierId: string, uid: string, name: string): void;
|
|
48
54
|
/**
|
|
49
55
|
* 读缓存的积分。没缓存过返回 `CREDITS_UNKNOWN`(-1),让调用方自己决定
|
|
50
56
|
* 显示什么——不要把 -1 当 0 展示。
|
|
@@ -102,10 +102,14 @@ export interface SupplierModule {
|
|
|
102
102
|
* 选号/回退/健康判定全是核心的活。
|
|
103
103
|
* 无账号供应商(如 opencode)传空 uid,忽略即可。
|
|
104
104
|
*
|
|
105
|
+
* `lv` 是推理等级(reasoning effort),`'auto'` 表示不显式下发;
|
|
106
|
+
* 供应商各自决定怎么把 `lv` 映射到上游(OpenAI 兼容一般是
|
|
107
|
+
* `reasoning_effort` 字段)。
|
|
108
|
+
*
|
|
105
109
|
* 失败原因通过返回值的 `message` 报(核心在测试模型时直接用它做诊断提示),
|
|
106
110
|
* 插件不必自己存一份「上次失败」的状态。
|
|
107
111
|
*/
|
|
108
|
-
chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult>;
|
|
112
|
+
chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult>;
|
|
109
113
|
dispose(): void;
|
|
110
114
|
/** 生成登录链接(加账号)。有它才显示连接池(账号池)。 */
|
|
111
115
|
generateLoginUrl?(): string | {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-router-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "DSH plugin: a simplified 9router (AI routing gateway). Adds a 路由系统 (Routing System) sidebar entry beside 记忆系统; the panel shows suppliers, account pool, combos, and endpoint/API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"react",
|
|
35
35
|
"react-dom",
|
|
36
36
|
"cordis",
|
|
37
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
38
37
|
"@deepseek-ai/dsh-client-ui-slots",
|
|
39
38
|
"@deepseek-ai/dsh-client-locale"
|
|
40
39
|
],
|
|
@@ -67,10 +66,9 @@
|
|
|
67
66
|
"license": "MIT",
|
|
68
67
|
"peerDependencies": {
|
|
69
68
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
70
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
71
|
-
"@deepseek-ai/dsh-client-
|
|
72
|
-
"@deepseek-ai/dsh-
|
|
73
|
-
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
|
|
74
72
|
"cordis": "^4.0.0-rc.7",
|
|
75
73
|
"react": "^18.2.0",
|
|
76
74
|
"react-dom": "^18.2.0"
|
|
@@ -82,24 +80,22 @@
|
|
|
82
80
|
"@deepseek-ai/dsh-client-locale": {
|
|
83
81
|
"optional": true
|
|
84
82
|
},
|
|
85
|
-
"@deepseek-ai/dsh-client-runtime": {
|
|
86
|
-
"optional": true
|
|
87
|
-
},
|
|
88
83
|
"@deepseek-ai/dsh-client-ui-slots": {
|
|
89
84
|
"optional": true
|
|
90
85
|
}
|
|
91
86
|
},
|
|
92
87
|
"devDependencies": {
|
|
93
|
-
"@
|
|
94
|
-
"@types/
|
|
95
|
-
"@types/react
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"react
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"vite
|
|
88
|
+
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
|
|
89
|
+
"@types/node": "^26.4.1",
|
|
90
|
+
"@types/react": "~19.2.18",
|
|
91
|
+
"@types/react-dom": "~19.2.7",
|
|
92
|
+
"cordis": "4.0.0-rc.9",
|
|
93
|
+
"react": "^19.2.8",
|
|
94
|
+
"react-dom": "19.2.8",
|
|
95
|
+
"tsdown": "^0.23.0",
|
|
96
|
+
"typescript": "^7.0.2",
|
|
97
|
+
"vite": "8.2.2",
|
|
98
|
+
"vite-plugin-css-injected-by-js": "^5.0.2"
|
|
103
99
|
},
|
|
104
100
|
"publishConfig": {
|
|
105
101
|
"registry": "https://registry.npmjs.org"
|
|
@@ -112,6 +112,8 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
112
112
|
const [checkingIn, setCheckingIn] = useState(false)
|
|
113
113
|
const [refreshing, setRefreshing] = useState(false)
|
|
114
114
|
const [dragIndex, setDragIndex] = useState<number | null>(null)
|
|
115
|
+
const [renameUid, setRenameUid] = useState<string | null>(null)
|
|
116
|
+
const [renameDraft, setRenameDraft] = useState('')
|
|
115
117
|
const mounted = useRef(true)
|
|
116
118
|
|
|
117
119
|
useEffect(() => {
|
|
@@ -343,6 +345,33 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
343
345
|
setRemoveTarget(null)
|
|
344
346
|
}
|
|
345
347
|
|
|
348
|
+
const startRename = (account: RouterAccount): void => {
|
|
349
|
+
setRenameDraft(account.nickname ?? '')
|
|
350
|
+
setRenameUid(account.uid)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const saveRename = async (account: RouterAccount): Promise<void> => {
|
|
354
|
+
try {
|
|
355
|
+
const response = await fetch(`${ROUTER_API_BASE}/status/rename`, {
|
|
356
|
+
method: 'POST',
|
|
357
|
+
headers: { 'Content-Type': 'application/json' },
|
|
358
|
+
body: JSON.stringify({ supplier: supplier.id, uid: account.uid, name: renameDraft.trim() }),
|
|
359
|
+
cache: 'no-store',
|
|
360
|
+
})
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
showToast(`保存失败:HTTP ${response.status}`)
|
|
363
|
+
setRenameUid(null)
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
onRefresh()
|
|
367
|
+
showToast('连接名已保存')
|
|
368
|
+
} catch (err) {
|
|
369
|
+
showToast(`保存失败:${(err as Error).message}`)
|
|
370
|
+
} finally {
|
|
371
|
+
setRenameUid(null)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
346
375
|
const toggleModel = async (id: string, enabled: boolean): Promise<void> => {
|
|
347
376
|
setToggling(true)
|
|
348
377
|
try {
|
|
@@ -644,7 +673,7 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
644
673
|
<div
|
|
645
674
|
key={account.uid}
|
|
646
675
|
className={`dshr-linkRow${dragIndex === index ? ' dshr-linkRowDragging' : ''}`}
|
|
647
|
-
draggable
|
|
676
|
+
draggable={renameUid !== account.uid}
|
|
648
677
|
onDragStart={(e) => { setDragIndex(index); e.dataTransfer.effectAllowed = 'move' }}
|
|
649
678
|
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move' }}
|
|
650
679
|
onDrop={(e) => {
|
|
@@ -667,7 +696,20 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
667
696
|
</div>
|
|
668
697
|
<div className="dshr-linkMain">
|
|
669
698
|
<div className="dshr-linkTitleLine">
|
|
670
|
-
|
|
699
|
+
{renameUid === account.uid
|
|
700
|
+
? (
|
|
701
|
+
<input
|
|
702
|
+
className="dshr-input dshr-mono"
|
|
703
|
+
value={renameDraft}
|
|
704
|
+
autoFocus
|
|
705
|
+
onChange={(e) => setRenameDraft(e.target.value)}
|
|
706
|
+
onKeyDown={(e) => {
|
|
707
|
+
if (e.key === 'Enter') void saveRename(account)
|
|
708
|
+
if (e.key === 'Escape') setRenameUid(null)
|
|
709
|
+
}}
|
|
710
|
+
/>
|
|
711
|
+
)
|
|
712
|
+
: <span className="dshr-linkName">{account.nickname || account.uid}</span>}
|
|
671
713
|
<span className={`dshr-badge ${badge.cls}`}>{badge.text}</span>
|
|
672
714
|
</div>
|
|
673
715
|
<div className="dshr-linkSub">
|
|
@@ -689,6 +731,17 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
689
731
|
</div>
|
|
690
732
|
</div>
|
|
691
733
|
<div className="dshr-linkOps">
|
|
734
|
+
<button
|
|
735
|
+
type="button"
|
|
736
|
+
className={`dshr-iconBtn dshr-iconBtn-sm dshr-linkEdit${renameUid === account.uid ? ' dshr-linkEdit-active' : ''}`}
|
|
737
|
+
title={renameUid === account.uid ? '保存名字' : '改名'}
|
|
738
|
+
onClick={() => {
|
|
739
|
+
if (renameUid === account.uid) void saveRename(account)
|
|
740
|
+
else startRename(account)
|
|
741
|
+
}}
|
|
742
|
+
>
|
|
743
|
+
<Icon d={renameUid === account.uid ? I.check : I.edit} size={15} />
|
|
744
|
+
</button>
|
|
692
745
|
<button type="button" className="dshr-iconBtn dshr-iconBtn-sm dshr-linkDelete" title="删除链接" onClick={() => setRemoveTarget(account)}>
|
|
693
746
|
<Icon d={I.delete} size={15} />
|
|
694
747
|
</button>
|
package/src/client/router.css
CHANGED
|
@@ -1634,6 +1634,20 @@ html[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-
|
|
|
1634
1634
|
background: color-mix(in srgb, var(--rs-danger) 14%, transparent);
|
|
1635
1635
|
}
|
|
1636
1636
|
|
|
1637
|
+
/* 改名:与删除一致,hover 行才显示;编辑中保持可见(否则保存按钮被藏住) */
|
|
1638
|
+
.dshr-linkEdit {
|
|
1639
|
+
color: var(--rs-muted);
|
|
1640
|
+
opacity: 0;
|
|
1641
|
+
transition: opacity .12s ease, color .12s ease, background .12s ease;
|
|
1642
|
+
}
|
|
1643
|
+
.dshr-linkRow:hover .dshr-linkEdit { opacity: 1; }
|
|
1644
|
+
.dshr-linkEdit:focus-visible { opacity: 1; }
|
|
1645
|
+
.dshr-linkEdit:hover {
|
|
1646
|
+
color: var(--rs-accent);
|
|
1647
|
+
background: color-mix(in srgb, var(--rs-accent) 14%, transparent);
|
|
1648
|
+
}
|
|
1649
|
+
.dshr-linkEdit.dshr-linkEdit-active { opacity: 1; }
|
|
1650
|
+
|
|
1637
1651
|
/* 模型 chips(9router ModelRow 风格) */
|
|
1638
1652
|
.dshr-modelChips {
|
|
1639
1653
|
display: flex;
|