dsh-router-core 0.3.9 → 0.3.11

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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/docs/suppliers.md +5 -1
  3. package/lib/client-registry.js +62 -8
  4. package/lib/client.js +62 -8
  5. package/lib/index.js +207 -18
  6. package/lib/suppliers/nvidia.js +6 -1
  7. package/lib/suppliers/opencode.js +6 -1
  8. package/lib/suppliers/openrouter.js +12 -2
  9. package/lib/types/data-dir.d.ts +11 -0
  10. package/lib/types/llm/adapter.d.ts +9 -5
  11. package/lib/types/router/index.d.ts +15 -0
  12. package/lib/types/router/types.d.ts +5 -1
  13. package/lib/types/shared.d.ts +2 -0
  14. package/lib/types/supplier-config.d.ts +6 -0
  15. package/lib/types/suppliers/contract.d.ts +26 -1
  16. package/package.json +1 -1
  17. package/src/client/RouterView.tsx +2 -2
  18. package/src/client/StatsTab.tsx +4 -2
  19. package/src/client/SupplierDetail.tsx +57 -4
  20. package/src/client/router.css +14 -0
  21. package/src/data-dir.test.ts +54 -0
  22. package/src/data-dir.ts +46 -0
  23. package/src/index.ts +53 -9
  24. package/src/llm/adapter.test.ts +34 -0
  25. package/src/llm/adapter.ts +34 -1
  26. package/src/router/index.ts +34 -2
  27. package/src/router/models-cache.test.ts +39 -1
  28. package/src/router/precise-dispatch.test.ts +2 -2
  29. package/src/router/types.ts +5 -1
  30. package/src/router/usage.test.ts +2 -2
  31. package/src/shared.ts +2 -0
  32. package/src/supplier-config.test.ts +15 -0
  33. package/src/supplier-config.ts +37 -1
  34. package/src/suppliers/contract.ts +27 -1
  35. package/src/suppliers/loader.test.ts +66 -1
  36. package/src/suppliers/loader.ts +29 -4
  37. package/src/suppliers/nvidia/plugin.ts +6 -1
  38. package/src/suppliers/opencode/plugin.ts +6 -1
  39. package/src/suppliers/openrouter/plugin.ts +11 -1
package/lib/index.js CHANGED
@@ -1,9 +1,10 @@
1
- import { basename, dirname, join } from "node:path";
1
+ import { basename, dirname, join, resolve } from "node:path";
2
2
  import { mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
3
- import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ToolCallId, attributionHeaders } from "@deepseek-ai/dsh-llm";
3
+ import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders } from "@deepseek-ai/dsh-llm";
4
4
  import { randomBytes } from "node:crypto";
5
- import { pathToFileURL } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { DatabaseSync } from "node:sqlite";
7
+ import { homedir } from "node:os";
7
8
  //#region src/shared.ts
8
9
  /** Same-origin API base the client panel fetches (host half serves it). */
9
10
  const ROUTER_API_BASE = "/router/api";
@@ -256,6 +257,13 @@ function readCredits(raw) {
256
257
  for (const [uid, v] of Object.entries(raw)) if (typeof v === "number" && Number.isFinite(v) && v >= 0) out[uid] = v;
257
258
  return out;
258
259
  }
260
+ /** 读盘时的连接显示别名校验:只留非空字符串。 */
261
+ function readNames(raw) {
262
+ const out = {};
263
+ if (typeof raw !== "object" || raw === null) return out;
264
+ for (const [uid, v] of Object.entries(raw)) if (typeof v === "string" && v.trim() !== "") out[uid] = v.trim();
265
+ return out;
266
+ }
259
267
  /** 通用供应商配置存储。 */
260
268
  var SupplierConfigStore = class {
261
269
  fp = "";
@@ -286,7 +294,8 @@ var SupplierConfigStore = class {
286
294
  custom: [],
287
295
  poolOrder: [],
288
296
  poolStrategy: "fallback",
289
- credits: {}
297
+ credits: {},
298
+ accountNames: {}
290
299
  };
291
300
  this.bySupplier.set(supplierId, cfg);
292
301
  }
@@ -338,6 +347,24 @@ var SupplierConfigStore = class {
338
347
  this.get(supplierId).poolStrategy = strategy;
339
348
  this.saveLocked();
340
349
  }
350
+ /** 读某连接的显示别名;没设或为空返回 `undefined`(用供应商原始昵称)。 */
351
+ getAccountName(supplierId, uid) {
352
+ const n = this.get(supplierId).accountNames[uid];
353
+ return typeof n === "string" && n !== "" ? n : void 0;
354
+ }
355
+ /** 设置连接显示名。空名 = 删除别名(回到供应商原始昵称)。 */
356
+ setAccountName(supplierId, uid, name) {
357
+ const cfg = this.get(supplierId);
358
+ const clean = (name ?? "").trim();
359
+ if (clean === "") {
360
+ if (cfg.accountNames[uid] === void 0) return;
361
+ delete cfg.accountNames[uid];
362
+ this.saveLocked();
363
+ return;
364
+ }
365
+ cfg.accountNames[uid] = clean;
366
+ this.saveLocked();
367
+ }
341
368
  /**
342
369
  * 读缓存的积分。没缓存过返回 `CREDITS_UNKNOWN`(-1),让调用方自己决定
343
370
  * 显示什么——不要把 -1 当 0 展示。
@@ -378,7 +405,8 @@ var SupplierConfigStore = class {
378
405
  custom: Array.isArray(c.custom) ? c.custom.filter((m) => typeof m === "string") : [],
379
406
  poolOrder: Array.isArray(c.poolOrder) ? c.poolOrder.filter((u) => typeof u === "string") : [],
380
407
  poolStrategy: c.poolStrategy === "round-robin" ? "round-robin" : "fallback",
381
- credits: readCredits(c.credits)
408
+ credits: readCredits(c.credits),
409
+ accountNames: readNames(c.accountNames)
382
410
  });
383
411
  }
384
412
  saveLocked() {
@@ -390,7 +418,8 @@ var SupplierConfigStore = class {
390
418
  custom: [...cfg.custom],
391
419
  poolOrder: [...cfg.poolOrder],
392
420
  poolStrategy: cfg.poolStrategy,
393
- credits: { ...cfg.credits }
421
+ credits: { ...cfg.credits },
422
+ accountNames: { ...cfg.accountNames }
394
423
  };
395
424
  try {
396
425
  const dir = dirname(this.fp);
@@ -1580,6 +1609,36 @@ var Router = class {
1580
1609
  * 只有两种情况会真的等:`force`(用户主动刷新)和**从来没有过缓存值**
1581
1610
  * (冷启动,没有旧值可给)。后者只发生一次。
1582
1611
  */
1612
+ /**
1613
+ * 组合的上下文窗口(token):组合内各模型窗口的**最小值**,拿不到就 undefined。
1614
+ *
1615
+ * 只读缓存、绝不打上游:调用方是 llm adapter 的 `resolveModel`,它在每次
1616
+ * 请求前后都可能被调用,不能为了报一个容量去触发按供应商的网络往返。
1617
+ * 冷启动(还没缓存)时给 undefined —— 自动压缩会因此跳过这一轮,下次就
1618
+ * 有了,比撒谎说一个窗口值安全。
1619
+ *
1620
+ * 单位:`ModelInfo.context_length` 存的是 **K**(供应商插件统一
1621
+ * `Math.round(tokens / 1000)`,见 codebuddy plugin),这里换算回 token。
1622
+ *
1623
+ * 取最小而不是最大:组合会按策略 fallback 到任意一个模型,按最窄的声明
1624
+ * 才能让「快满了」这件事在任何一条分支上都成立。
1625
+ */
1626
+ comboContextWindow(combo) {
1627
+ let min;
1628
+ for (const ref of combo.models) {
1629
+ const at = ref.indexOf(",");
1630
+ if (at < 0) continue;
1631
+ const supplierId = ref.slice(0, at);
1632
+ const modelId = ref.slice(at + 1);
1633
+ const hit = this.modelsCache.get(supplierId);
1634
+ if (hit === void 0) return void 0;
1635
+ const k = hit.models.find((x) => x.id === modelId)?.context_length;
1636
+ if (typeof k !== "number" || !Number.isFinite(k) || k <= 0) continue;
1637
+ const tokens = Math.round(k * 1e3);
1638
+ if (min === void 0 || tokens < min) min = tokens;
1639
+ }
1640
+ return min;
1641
+ }
1583
1642
  async modelsOf(supplierId, force = false) {
1584
1643
  const s = this.suppliers.find((x) => x.id === supplierId);
1585
1644
  if (s === void 0) return [];
@@ -1919,7 +1978,7 @@ var Router = class {
1919
1978
  const pool = s.pool;
1920
1979
  const cfg = this.store.get(s.id);
1921
1980
  if (s.accounts !== void 0 && s.accounts().length === 0) {
1922
- const r = await s.chatOnce("", req);
1981
+ const r = await s.chatOnce("", req.lv ?? "auto", req);
1923
1982
  if (!r.ok) {
1924
1983
  if (r.state === "no_such_model") return false;
1925
1984
  if (trace !== void 0) trace.lastState = r.state;
@@ -1939,7 +1998,7 @@ var Router = class {
1939
1998
  const uid = pool.pick(s.accounts().filter((a) => !tried.has(a.uid)), cfg.poolOrder, cfg.poolStrategy, req.model);
1940
1999
  if (uid === void 0) return false;
1941
2000
  tried.add(uid);
1942
- const r = await s.chatOnce(uid, req);
2001
+ const r = await s.chatOnce(uid, req.lv ?? "auto", req);
1943
2002
  if (!r.ok) {
1944
2003
  if (r.state === "no_such_model") {
1945
2004
  if (trace !== void 0) trace.lastError = "no_such_model";
@@ -2342,6 +2401,25 @@ function completeJson(text) {
2342
2401
  * DSH adapter:provider `router`。模型目录 = 组合;stream 转发到本插件
2343
2402
  * /v1/chat/completions(组合路由在 /v1 内完成)。
2344
2403
  */
2404
+ /** Router provider 对外暴露的推理等级(对齐 dsh/DeepSeek:off/low/high/max)。 */
2405
+ const ROUTER_REASONING_EFFORTS = [
2406
+ {
2407
+ id: ReasoningEffortId("off"),
2408
+ name: "Off"
2409
+ },
2410
+ {
2411
+ id: ReasoningEffortId("low"),
2412
+ name: "Low"
2413
+ },
2414
+ {
2415
+ id: ReasoningEffortId("high"),
2416
+ name: "High"
2417
+ },
2418
+ {
2419
+ id: ReasoningEffortId("max"),
2420
+ name: "Max"
2421
+ }
2422
+ ];
2345
2423
  var RouterAdapter = class extends LlmAdapter {
2346
2424
  baseURL;
2347
2425
  source;
@@ -2371,6 +2449,19 @@ var RouterAdapter = class extends LlmAdapter {
2371
2449
  }
2372
2450
  return out;
2373
2451
  }
2452
+ async resolveModel(_provider, model) {
2453
+ const found = (await this.source.comboModels()).find((m) => m.id === model || m.name === model);
2454
+ return {
2455
+ provider: "router",
2456
+ id: model,
2457
+ name: found?.name ?? found?.id ?? model,
2458
+ ...found?.contextWindow !== void 0 ? { context: { contextWindow: found.contextWindow } } : {},
2459
+ reasoning: {
2460
+ efforts: ROUTER_REASONING_EFFORTS,
2461
+ defaultEffort: ReasoningEffortId("high")
2462
+ }
2463
+ };
2464
+ }
2374
2465
  async *stream(options) {
2375
2466
  const body = wireRequest(options);
2376
2467
  const controller = new AbortController();
@@ -2637,15 +2728,24 @@ const DIFF_CAPS = [
2637
2728
  "pollLogin",
2638
2729
  "checkinNow"
2639
2730
  ];
2731
+ /**
2732
+ * 每个供应商一份 env 副本:`onLateFailure` 是**按供应商**绑到本供应商的池上
2733
+ * 的(见 wrapModule),共用一个 env 对象会让所有插件的上报都落到最后一个
2734
+ * 供应商的池里。副本是浅拷贝,store/credentials 仍是同一实例。
2735
+ */
2736
+ function ownEnv(env) {
2737
+ return { ...env };
2738
+ }
2640
2739
  /** 从文件构造 LoadedSupplier。 */
2641
2740
  async function loadOne(file, env, source) {
2642
2741
  const mod = await importSupplierFile(file);
2742
+ const mine = ownEnv(env);
2643
2743
  let instance;
2644
2744
  if (typeof mod === "function") {
2645
- instance = mod(env);
2745
+ instance = mod(mine);
2646
2746
  if (typeof instance !== "object" || instance === null || typeof instance.id !== "string" || instance.id === "") throw new Error(`供应商 factory 返回无效: ${file}`);
2647
2747
  } else instance = mod;
2648
- return wrapModule(instance, env, file, source);
2748
+ return wrapModule(instance, mine, file, source);
2649
2749
  }
2650
2750
  /**
2651
2751
  * 从 SupplierModule 实例构造 LoadedSupplier(能力检测 + Supplier 包装)。
@@ -2656,17 +2756,29 @@ function wrapModule(instance, env, source, kind = "external") {
2656
2756
  const capabilities = /* @__PURE__ */ new Set();
2657
2757
  for (const key of DIFF_CAPS) if (isFn(instance[key])) capabilities.add(key);
2658
2758
  const pool = new AccountPool(instance.id);
2759
+ env.onLateFailure = (uid, model, state, message) => {
2760
+ if (uid === "") return;
2761
+ pool.noteFailure(uid, model, state, message);
2762
+ };
2659
2763
  const supplier = {
2660
2764
  id: instance.id,
2661
2765
  name: instance.name,
2662
2766
  priority: instance.priority ?? 0,
2663
2767
  icon: instance.icon,
2768
+ apiKeyHint: instance.apiKeyHint,
2664
2769
  status: () => {
2665
2770
  const now = instance.status();
2771
+ const accounts = now.accounts.map((a) => {
2772
+ const alias = env.store.getAccountName(instance.id, a.uid);
2773
+ return alias === void 0 || alias === a.nickname ? a : {
2774
+ ...a,
2775
+ nickname: alias
2776
+ };
2777
+ });
2666
2778
  return {
2667
2779
  id: now.id,
2668
2780
  name: now.name,
2669
- accounts: pool.decorate(hydrateCredits(instance.id, env.store, now.accounts))
2781
+ accounts: pool.decorate(hydrateCredits(instance.id, env.store, accounts))
2670
2782
  };
2671
2783
  },
2672
2784
  listModels: (force) => instance.listModels(force),
@@ -2683,7 +2795,7 @@ function wrapModule(instance, env, source, kind = "external") {
2683
2795
  getAlias: () => env.store.get(instance.id).alias || instance.id,
2684
2796
  customModelIds: () => [...env.store.get(instance.id).custom],
2685
2797
  accounts: () => instance.status().accounts,
2686
- chatOnce: (uid, req) => instance.chatOnce(uid, req),
2798
+ chatOnce: (uid, lv, req) => instance.chatOnce(uid, lv, req),
2687
2799
  removeLink: isFn(instance.removeLink) ? (uid) => instance.removeLink(uid) : void 0,
2688
2800
  dispose: () => instance.dispose(),
2689
2801
  pool
@@ -3179,6 +3291,43 @@ var CredentialStore = class {
3179
3291
  }
3180
3292
  };
3181
3293
  //#endregion
3294
+ //#region src/data-dir.ts
3295
+ /**
3296
+ * 落盘路径解析 —— dsh-router 所有持久化路径的唯一来源。
3297
+ *
3298
+ * 以前写的是相对路径 `data/state.json`:**落盘位置跟着 `process.cwd()` 跑**。
3299
+ * 不在 `~/.dsh/profiles/web` 里 `dsh web` 启动,state.json / supplier-config.json
3300
+ * / keys.json / usage.json 就会静默写进另一个目录(面板显示空配置),切回原目录
3301
+ * 启动又「回来」——用户看到的是「配置丢了」。所以这里一律产出绝对路径。
3302
+ */
3303
+ /**
3304
+ * 本 profile 的目录。
3305
+ *
3306
+ * 宿主把配置树锚点 `ctx.baseUrl` 挂在每个插件的 context 上,它正是 profile
3307
+ * 目录(`cordis.yml` 所在处)——不用猜、也不用像 dsh-market 那样嗅探 argv。
3308
+ * 兜底只服务「宿主没给锚点」的冷启动(单测、非常规宿主):沿用今天的事实
3309
+ * 标准 `web` profile。
3310
+ */
3311
+ function profileDirOf(baseUrl) {
3312
+ if (typeof baseUrl === "string" && baseUrl !== "") try {
3313
+ return resolve(fileURLToPath(baseUrl));
3314
+ } catch {}
3315
+ return join(dshHome(), "profiles", "web");
3316
+ }
3317
+ /** 数据目录:state.json / supplier-config.json / keys.json / auths/ 都在这里。 */
3318
+ function dataDirOf(baseUrl) {
3319
+ return join(profileDirOf(baseUrl), "data");
3320
+ }
3321
+ /** `$DSH_HOME`(支持 `~` 前缀),缺省 `~/.dsh`。 */
3322
+ function dshHome() {
3323
+ const raw = process.env.DSH_HOME;
3324
+ if (raw === void 0 || raw.trim() === "") return join(homedir(), ".dsh");
3325
+ const value = raw.trim();
3326
+ if (value === "~") return homedir();
3327
+ if (value.startsWith("~/") || value.startsWith("~\\")) return resolve(homedir(), value.slice(2));
3328
+ return resolve(value);
3329
+ }
3330
+ //#endregion
3182
3331
  //#region src/index.ts
3183
3332
  /**
3184
3333
  * Plugin identity for cordis.yml rows — 必须与 package.json 的 name 一致。
@@ -3216,9 +3365,11 @@ function readBody(req, limit) {
3216
3365
  function apply(rawContext) {
3217
3366
  const ctx = rawContext;
3218
3367
  const log = (msg) => ctx.logger.info(`[dsh-router] ${msg}`);
3219
- const stateFile = "data/state.json";
3368
+ const dataDir = dataDirOf(ctx.baseUrl);
3369
+ const stateFile = join(dataDir, "state.json");
3370
+ log(`data dir: ${dataDir}`);
3220
3371
  const store = new SupplierConfigStore(stateFile);
3221
- const credentials = new CredentialStore(join(dirname(stateFile), "auths"));
3372
+ const credentials = new CredentialStore(join(dataDir, "auths"));
3222
3373
  const router = new Router(stateFile, store, log);
3223
3374
  const keys = new KeysStore(stateFile);
3224
3375
  const disposers = [];
@@ -3250,7 +3401,6 @@ function apply(rawContext) {
3250
3401
  log(`supplier unregistered: ${id}`);
3251
3402
  };
3252
3403
  ctx.provide("router.suppliers", {});
3253
- const dataDir = dirname(stateFile);
3254
3404
  const registerLoaded = (loaded) => {
3255
3405
  loadedSuppliers.push(loaded);
3256
3406
  router.add(loaded.supplier);
@@ -3296,7 +3446,7 @@ function apply(rawContext) {
3296
3446
  });
3297
3447
  (async () => {
3298
3448
  try {
3299
- const userDir = join(join(process.env.HOME ?? "", ".dsh", "profiles", "web"), "suppliers");
3449
+ const userDir = join(profileDirOf(ctx.baseUrl), "suppliers");
3300
3450
  const { suppliers, errors } = await loadSuppliers({
3301
3451
  builtinDir: join(import.meta.dirname, "suppliers"),
3302
3452
  userDir,
@@ -3356,10 +3506,12 @@ function apply(rawContext) {
3356
3506
  } });
3357
3507
  return;
3358
3508
  }
3509
+ const lv = typeof peek.reasoning_effort === "string" && peek.reasoning_effort !== "" ? peek.reasoning_effort : "auto";
3359
3510
  await router.chatCompletions({
3360
3511
  rawBody: body,
3361
3512
  stream: !!peek.stream,
3362
- model: typeof peek.model === "string" ? peek.model : ""
3513
+ model: typeof peek.model === "string" ? peek.model : "",
3514
+ lv
3363
3515
  }, res);
3364
3516
  }));
3365
3517
  route(`${ROUTER_API_BASE}/health`, (_req, res) => {
@@ -3372,6 +3524,7 @@ function apply(rawContext) {
3372
3524
  id: s.id,
3373
3525
  name: s.name,
3374
3526
  icon: loaded?.supplier.icon,
3527
+ apiKeyHint: loaded?.supplier.apiKeyHint,
3375
3528
  capabilities: loaded ? [...loaded.capabilities] : [],
3376
3529
  source: loaded?.source ?? "external"
3377
3530
  };
@@ -3388,6 +3541,27 @@ function apply(rawContext) {
3388
3541
  })))
3389
3542
  });
3390
3543
  });
3544
+ route(`${ROUTER_API_BASE}/status/rename`, async (req, res) => {
3545
+ let body;
3546
+ try {
3547
+ body = JSON.parse(await readBody(req, 65536));
3548
+ } catch {
3549
+ writeJson(res, 400, {
3550
+ ok: false,
3551
+ error: "invalid JSON body"
3552
+ });
3553
+ return;
3554
+ }
3555
+ if (typeof body.supplier !== "string" || body.supplier === "" || typeof body.uid !== "string" || body.uid === "") {
3556
+ writeJson(res, 400, {
3557
+ ok: false,
3558
+ error: "supplier and uid are required"
3559
+ });
3560
+ return;
3561
+ }
3562
+ store.setAccountName(body.supplier, body.uid, typeof body.name === "string" ? body.name : "");
3563
+ writeJson(res, 200, { ok: true });
3564
+ });
3391
3565
  route(`${ROUTER_API_BASE}/models`, async (_req, res) => {
3392
3566
  try {
3393
3567
  writeJson(res, 200, {
@@ -3587,8 +3761,23 @@ function apply(rawContext) {
3587
3761
  disposers.push(ctx.llm.registerModelDiscovery(LLM_NS, async () => {
3588
3762
  return (await router.combos()).map((c) => ({ id: c.name }));
3589
3763
  }));
3590
- disposers.push(ctx.llm.registerAdapter(["router"], new RouterAdapter("http://localhost:3080/v1", { comboModels: async () => (await router.combos()).map((c) => ({ id: c.name })) })));
3764
+ disposers.push(ctx.llm.registerAdapter(["router"], new RouterAdapter("http://localhost:3080/v1", { comboModels: async () => (await router.combos()).map((c) => {
3765
+ const w = router.comboContextWindow(c);
3766
+ return {
3767
+ id: c.name,
3768
+ ...w !== void 0 ? { contextWindow: w } : {}
3769
+ };
3770
+ }) })));
3591
3771
  log("llm provider (Router) + discovery + adapter registered ok");
3772
+ (async () => {
3773
+ for (const c of await router.combos()) for (const ref of c.models) {
3774
+ const at = ref.indexOf(",");
3775
+ if (at < 0) continue;
3776
+ try {
3777
+ await router.modelsOf(ref.slice(0, at));
3778
+ } catch {}
3779
+ }
3780
+ })().catch(() => {});
3592
3781
  } catch (err) {
3593
3782
  ctx.logger.warn(`[dsh-router] llm registration failed: ${err.message}`);
3594
3783
  }
@@ -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;
@@ -4,6 +4,10 @@ const name = "OpenRouter";
4
4
  const priority = 10;
5
5
  /** 面板图标。 */
6
6
  const icon = "data:image/png;base64,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";
7
+ /**
8
+ * 「添加 API key」弹窗提示(OpenRouter API key 获取页)。
9
+ */
10
+ const apiKeyHint = "填入 OpenRouter API key(https://openrouter.ai/settings/keys),可起名字区分多个 key。";
7
11
  const BASE = "https://openrouter.ai/api/v1";
8
12
  const MODELS_URL = `${BASE}/models`;
9
13
  const CHAT_URL = `${BASE}/chat/completions`;
@@ -79,6 +83,7 @@ function factory(env) {
79
83
  name,
80
84
  priority: 10,
81
85
  icon,
86
+ apiKeyHint,
82
87
  status: () => {
83
88
  const accounts = orderedKeys().map(({ uid, acct }) => ({
84
89
  uid,
@@ -142,7 +147,7 @@ function factory(env) {
142
147
  return true;
143
148
  },
144
149
  /** 对单个 key 调一次上游。选号/冷却/换号是核心的活,这里只报结果。 */
145
- async chatOnce(uid, req) {
150
+ async chatOnce(uid, lv, req) {
146
151
  const base = stripAlias(req.model, currentAlias());
147
152
  if (!freeIds.has(base) && !(await allModels(false)).some((m) => m.id === base)) return {
148
153
  ok: false,
@@ -159,6 +164,11 @@ function factory(env) {
159
164
  try {
160
165
  const obj = JSON.parse(body);
161
166
  obj.model = base;
167
+ if (lv !== "auto" && lv !== "" && lv !== "none" && lv !== "off") obj.reasoning_effort = lv;
168
+ else if (lv === "none" || lv === "off") {
169
+ delete obj.reasoning_effort;
170
+ delete obj.reasoning_summary;
171
+ }
162
172
  body = JSON.stringify(obj);
163
173
  } catch {}
164
174
  let upstream;
@@ -219,4 +229,4 @@ function factory(env) {
219
229
  };
220
230
  }
221
231
  //#endregion
222
- export { factory as default, icon, id, name, priority };
232
+ export { apiKeyHint, factory as default, icon, id, name, priority };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 本 profile 的目录。
3
+ *
4
+ * 宿主把配置树锚点 `ctx.baseUrl` 挂在每个插件的 context 上,它正是 profile
5
+ * 目录(`cordis.yml` 所在处)——不用猜、也不用像 dsh-market 那样嗅探 argv。
6
+ * 兜底只服务「宿主没给锚点」的冷启动(单测、非常规宿主):沿用今天的事实
7
+ * 标准 `web` profile。
8
+ */
9
+ export declare function profileDirOf(baseUrl?: string): string;
10
+ /** 数据目录:state.json / supplier-config.json / keys.json / auths/ 都在这里。 */
11
+ export declare function dataDirOf(baseUrl?: string): string;
@@ -6,12 +6,19 @@
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
+ /**
13
+ * 组合列表。`contextWindow` 可选,但**必须尽量给**——自动压缩靠它算阈值
14
+ * (`dsh-compaction-basic` 在 `context` 缺失时直接抛错并静默关闭自动压缩,
15
+ * 于是上下文会一路涨到模型硬上限才炸)。
16
+ * 组合背后是异构供应商时给**最小的那个**(保守,宁可早压缩)。
17
+ */
12
18
  comboModels: () => Promise<Array<{
13
19
  id: string;
14
20
  name?: string;
21
+ contextWindow?: number;
15
22
  }>>;
16
23
  }
17
24
  /** openai SSE payload → DSH StreamChunk 流。导出以便单测锁死 usage 契约。 */
@@ -22,10 +29,6 @@ export interface RouterAdapterSource {
22
29
  * 用 `for await` 是因为它天然兼容两者 —— 不必为流式再写一份。
23
30
  */
24
31
  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
32
  export declare class RouterAdapter extends LlmAdapter {
30
33
  private readonly baseURL;
31
34
  private readonly source;
@@ -35,5 +38,6 @@ export declare class RouterAdapter extends LlmAdapter {
35
38
  name: string;
36
39
  };
37
40
  listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
41
+ resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
38
42
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
39
43
  }
@@ -66,6 +66,21 @@ export declare class Router {
66
66
  * 只有两种情况会真的等:`force`(用户主动刷新)和**从来没有过缓存值**
67
67
  * (冷启动,没有旧值可给)。后者只发生一次。
68
68
  */
69
+ /**
70
+ * 组合的上下文窗口(token):组合内各模型窗口的**最小值**,拿不到就 undefined。
71
+ *
72
+ * 只读缓存、绝不打上游:调用方是 llm adapter 的 `resolveModel`,它在每次
73
+ * 请求前后都可能被调用,不能为了报一个容量去触发按供应商的网络往返。
74
+ * 冷启动(还没缓存)时给 undefined —— 自动压缩会因此跳过这一轮,下次就
75
+ * 有了,比撒谎说一个窗口值安全。
76
+ *
77
+ * 单位:`ModelInfo.context_length` 存的是 **K**(供应商插件统一
78
+ * `Math.round(tokens / 1000)`,见 codebuddy plugin),这里换算回 token。
79
+ *
80
+ * 取最小而不是最大:组合会按策略 fallback 到任意一个模型,按最窄的声明
81
+ * 才能让「快满了」这件事在任何一条分支上都成立。
82
+ */
83
+ comboContextWindow(combo: Combo): number | undefined;
69
84
  modelsOf(supplierId: string, force?: boolean): Promise<ModelWithEnabled[]>;
70
85
  /**
71
86
  * 真拉一次上游并写缓存。并发调用共享同一个 in-flight promise——
@@ -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
  * 供应商接口。核心负责账号池(选号/冷却/禁用/错误累计/遍历回退),
@@ -60,6 +62,8 @@ export interface Supplier {
60
62
  readonly priority: number;
61
63
  /** 图标(URL 或 SVG data URI),面板供应商卡片显示。可选。 */
62
64
  readonly icon?: string;
65
+ /** 「添加 API key」弹窗提示文案(缺省前端用通用文案)。可选。 */
66
+ readonly apiKeyHint?: string;
63
67
  status(): SupplierStatus;
64
68
  listModels(force?: boolean): Promise<ModelInfo[]> | ModelInfo[];
65
69
  /** 面板展示的模型(含启用/自定义标记)。 */
@@ -71,7 +75,7 @@ export interface Supplier {
71
75
  /** 账号「现在状态」(从 status() 派生,插件只报它观察到的)。空 = 无账号供应商。 */
72
76
  accounts(): SupplierAccountNow[];
73
77
  /** 对单个账号调一次上游。不遍历账号、不管冷却、不写 res。 */
74
- chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult>;
78
+ chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult>;
75
79
  /** 删除链接(清理凭证;内部能力,js 契约不要求实现)。 */
76
80
  removeLink?(uid: string): Promise<boolean>;
77
81
  dispose(): void;
@@ -68,6 +68,8 @@ export interface RouterHealthResponse {
68
68
  name: string;
69
69
  /** 图标(URL 或 SVG data URI)。 */
70
70
  icon?: string;
71
+ /** 「添加 API key」弹窗提示文案(缺省前端用通用文案)。 */
72
+ apiKeyHint?: string;
71
73
  /** 面板能力集合(登录/签到/连接池/模型管理等,按存在性)。 */
72
74
  capabilities?: string[];
73
75
  /** 来源:内置 / 用户目录 / 外部插件。 */
@@ -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 展示。