mslxdff 0.1.155 → 0.1.158

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 (151) hide show
  1. package/bin/mslxdff.js +4 -4
  2. package/docs/ARCHITECTURE.md +426 -0
  3. package/docs/FEATURE_TREE.md +164 -0
  4. package/docs/MOBILE.md +82 -0
  5. package/docs/adr/0001-reasoning-content-injection.md +14 -0
  6. package/docs/adr/0002-models-free-filter.md +12 -0
  7. package/docs/adr/0003-zero-state-no-auth.md +10 -0
  8. package/docs/adr/0004-bearer-token.md +18 -0
  9. package/docs/adr/0005-peer-mesh.md +53 -0
  10. package/docs/adr/0006-broadband-member.md +103 -0
  11. package/docs/adr/0007-multi-provider-prefix.md +25 -0
  12. package/docs/adr/0008-share-keys-to-peers.md +52 -0
  13. package/docs/adr/0009-chat-repl.md +37 -0
  14. package/docs/adr/0010-allowlist.md +36 -0
  15. package/docs/adr/0011-broadband-stream.md +30 -0
  16. package/docs/adr/0012-responses-endpoint-codex-sync.md +48 -0
  17. package/docs/adr/0013-node16-compat.md +41 -0
  18. package/docs/adr/0014-deepseek-provider.md +52 -0
  19. package/docs/adr/0015-upstream-probe-routing.md +50 -0
  20. package/docs/adr/0016-model-capabilities.md +27 -0
  21. package/docs/adr/0017-ai-sdk-upstream-engine.md +59 -0
  22. package/docs/adr/0018-zen-client-identity.md +52 -0
  23. package/docs/adr/0019-share-keys-always-lend.md +59 -0
  24. package/docs/adr/0020-zen-free-lane-agent-shape.md +52 -0
  25. package/docs/adr/0021-usage-report-jsonl.md +47 -0
  26. package/docs/adr/0022-models-capability-merge.md +61 -0
  27. package/docs/adr/0023-key-provider-default-direct.md +58 -0
  28. package/docs/adr/0024-node18-baseline.md +63 -0
  29. package/docs/adr/0025-workbuddy-authdir-follows-state.md +71 -0
  30. package/docs/adr/0026-cline-provider-id-unify.md +67 -0
  31. package/docs/adr/0027-codearts-provider.md +48 -0
  32. package/docs/adr/0028-traework-provider.md +34 -0
  33. package/docs/adr/0029-qoder-native-provider.md +60 -0
  34. package/docs/adr/0030-models-list-scoped-by-picks.md +53 -0
  35. package/docs/adr/0031-qoder-true-streaming.md +50 -0
  36. package/docs/adr/0032-generic-responses-channel.md +72 -0
  37. package/docs/adr/0033-cline-allowlist-auto-sync.md +75 -0
  38. package/docs/adr/0034-request-level-human-readable-observability.md +60 -0
  39. package/docs/adr/0035-sdk-channel-headers-timeout.md +49 -0
  40. package/docs/adr/0036-qoder-per-request-sticky-account.md +82 -0
  41. package/docs/adr/0037-qwenwork-independent-provider.md +82 -0
  42. package/docs/adr/0038-zcode-provider.md +140 -0
  43. package/docs/agents/domain.md +51 -0
  44. package/docs/agents/issue-tracker.md +30 -0
  45. package/docs/agents/triage-labels.md +15 -0
  46. package/docs/cli_help.md +1391 -0
  47. package/docs/cli_help_mini.md +133 -0
  48. package/docs/plans/bench-via-latency-2026-09-01.md +215 -0
  49. package/docs/plugins.md +187 -0
  50. package/package.json +1 -1
  51. package/src/auto.js +254 -254
  52. package/src/bench/cline-bench.js +42 -42
  53. package/src/bench/probe.js +70 -70
  54. package/src/bench/report.js +162 -162
  55. package/src/bench/runner.js +77 -77
  56. package/src/bench/via-probe.js +124 -124
  57. package/src/bench/via-routes.js +87 -87
  58. package/src/bench/workbuddy-bench.js +54 -54
  59. package/src/chat/engine.js +160 -160
  60. package/src/chat/gateway.js +163 -163
  61. package/src/chat/orchestrator.js +234 -234
  62. package/src/chat/prompt.js +70 -70
  63. package/src/chat/repl.js +88 -88
  64. package/src/chat/terminal.js +135 -135
  65. package/src/chat/tools.js +306 -306
  66. package/src/chat-pipeline/index.js +123 -123
  67. package/src/chat-pipeline/policy.js +76 -76
  68. package/src/chat-pipeline/serial-trial.js +210 -210
  69. package/src/cli/commands/group.js +249 -249
  70. package/src/cli/commands/model/list-providers.js +4 -3
  71. package/src/cli/commands/model/list-render.js +7 -9
  72. package/src/cli/commands/model/list-sort.js +52 -0
  73. package/src/cli/commands/model/list-tty.js +76 -0
  74. package/src/cli/commands/model/list.js +5 -62
  75. package/src/cli/commands/model/picks.js +50 -50
  76. package/src/cli/commands/provider/bench-via.js +247 -247
  77. package/src/cli/commands/provider/bench.js +141 -141
  78. package/src/cli/commands/provider/index.js +124 -116
  79. package/src/cli/commands/provider/models.js +139 -128
  80. package/src/cli/commands/provider/qwenwork-login.js +119 -0
  81. package/src/cli/commands/provider/zcode-login.js +77 -0
  82. package/src/cli/commands/provider/zcode-quota.js +55 -0
  83. package/src/cli/commands/sync.js +232 -232
  84. package/src/cli/provider-row.js +2 -2
  85. package/src/cli/status.js +279 -279
  86. package/src/daemon.js +96 -96
  87. package/src/model-capabilities/enrich.js +86 -86
  88. package/src/model-capabilities/index.js +183 -183
  89. package/src/model-capabilities/parse.js +70 -70
  90. package/src/model-trace.js +1 -0
  91. package/src/models.js +225 -225
  92. package/src/providers/classify.js +1 -1
  93. package/src/providers/cline/auth.js +228 -228
  94. package/src/providers/cline/chat.js +307 -307
  95. package/src/providers/cline.js +2 -2
  96. package/src/providers/keyring.js +60 -56
  97. package/src/providers/qoder/chat.js +183 -100
  98. package/src/providers/qoder/index.js +230 -184
  99. package/src/providers/qoder/sse.js +103 -46
  100. package/src/providers/qoder/sticky.js +1 -0
  101. package/src/providers/qoder/stream.js +32 -9
  102. package/src/providers/qwenwork/account-store.js +133 -0
  103. package/src/providers/qwenwork/constants.js +67 -0
  104. package/src/providers/qwenwork/cosy.js +120 -0
  105. package/src/providers/qwenwork/crypto.js +218 -0
  106. package/src/providers/qwenwork/http.js +20 -0
  107. package/src/providers/qwenwork/index.js +327 -0
  108. package/src/providers/qwenwork/payload.js +142 -0
  109. package/src/providers/qwenwork/rsa.js +54 -0
  110. package/src/providers/qwenwork/sse.js +268 -0
  111. package/src/providers/qwenwork/stream.js +130 -0
  112. package/src/providers/qwenwork/upstream.js +120 -0
  113. package/src/providers/qwenwork.js +1 -0
  114. package/src/providers/registry.js +66 -56
  115. package/src/providers/share-keys.js +2 -2
  116. package/src/providers/workbuddy/chat.js +248 -248
  117. package/src/providers/workbuddy/reshape.js +152 -152
  118. package/src/providers/workbuddy.js +2 -2
  119. package/src/providers/zcode/account-store.js +129 -0
  120. package/src/providers/zcode/auth.js +28 -0
  121. package/src/providers/zcode/chat.js +171 -0
  122. package/src/providers/zcode/const.js +54 -0
  123. package/src/providers/zcode/headers.js +55 -0
  124. package/src/providers/zcode/index.js +124 -0
  125. package/src/providers/zcode/models.js +65 -0
  126. package/src/providers/zcode/oauth.js +120 -0
  127. package/src/providers/zcode/quota.js +176 -0
  128. package/src/providers/zcode/sse.js +179 -0
  129. package/src/reasoning.js +32 -32
  130. package/src/routes/chat/gateway.js +46 -46
  131. package/src/routes/chat/relay-pipeline.js +250 -246
  132. package/src/routes/chat/via-route-handler.js +144 -144
  133. package/src/routes/hedge.js +255 -255
  134. package/src/routes/models-route.js +167 -167
  135. package/src/routes/peers.js +273 -273
  136. package/src/routes/stream.js +438 -399
  137. package/src/runtime/bootstrap.js +45 -45
  138. package/src/runtime/provider-gate.js +33 -30
  139. package/src/runtime/providers-setup.js +165 -165
  140. package/src/server.js +64 -64
  141. package/src/state/schemas/allowlist.js +92 -92
  142. package/src/sync-opencode.js +280 -280
  143. package/src/transport/index.js +244 -244
  144. package/src/transport/pool.js +56 -56
  145. package/src/transport/retry.js +24 -24
  146. package/src/transport/sse.js +93 -93
  147. package/src/upstream-probe/display.js +52 -52
  148. package/src/upstream-probe/probe.js +49 -49
  149. package/src/upstream-probe/rotate.js +110 -110
  150. package/src/upstream-probe/start.js +45 -45
  151. package/src/upstream.js +289 -289
@@ -1,167 +1,167 @@
1
- import { json, errMsg } from "./helpers.js";
2
- import { runHook } from "../plugins.js";
3
- import { isModelAllowed } from "../state.js";
4
- import { mergeModelsList } from "../model-capabilities/merge.js";
5
- import { globalCapabilities } from "../model-capabilities/index.js";
6
-
7
- // Codex 自定义 provider 拉目录要顶层 `models` 数组(codex-rs endpoint/models.rs 解 ModelsResponse{models}),
8
- // 给它 OpenAI 标准 {object,data} 会报 missing field `models`。学 OmniRoute:仅 codex 调用者追加空数组
9
- // (填真目录反而会覆盖 codex 内置 agent prompt,必须空),其他客户端保持字节一致。
10
- export function isCodexModelsCaller(req) {
11
- const h = req?.headers || {};
12
- if (/^codex_/i.test(String(h["user-agent"] || ""))) return true;
13
- if (/^codex_/i.test(String(h.originator || ""))) return true;
14
- const q = String(req?.url || "").split("?")[1] || "";
15
- return /(^|&)client_version=/.test(q);
16
- }
17
-
18
- // 勾选即对外目录:modelPicks 非空时 /v1/models 只暴露勾选的模型(与 -models 交互勾选语义闭环);
19
- // 空勾选 = 不过滤(保留旧默认全量目录,防呆:新装/清空勾选不至于对外空目录)。?all=1 逃生门。
20
- // Note: 空勾选不裁剪 + ?all=1 逃生门是硬边界(缺一即成单向棘轮/下游零模型)— 见 .agents/notes/implemented/architecture/2026-09-21-models-list-scoped-by-picks.md
21
- export function filterByPicks(data, picks) {
22
- if (!picks?.length) return data;
23
- const set = new Set(picks.map(String));
24
- const list = (Array.isArray(data?.data) ? data.data : []).filter((m) => set.has(String(m?.id || "")));
25
- return { ...(data || { object: "list" }), data: list };
26
- }
27
-
28
- export async function modelsHandler({ req, res, models, plugins, capabilities, wbSource, loadPicks } = {}) {
29
- if (!models) return json(res, 501, { error: "Models service not configured" });
30
- const codex = isCodexModelsCaller(req);
31
- const withCodex = (out) => (codex && out && typeof out === "object" ? { ...out, models: [] } : out);
32
- try {
33
- let data = await models.get();
34
- // ADR-0022:默认把能力富化进每条条目(capabilities 子对象);?raw=1 逃生门回原始形状,
35
- // codex 调用者保持原始 + 顶层空 models:[](富化白做)。best-effort,失败原样返回。
36
- const qs = String(req?.url || "").split("?")[1] || "";
37
- const rawMode = /(^|&)raw=1(&|$)/.test(qs);
38
- if (!codex && !rawMode) {
39
- try {
40
- data = await mergeModelsList(data, { capsSvc: capabilities, wbSource });
41
- } catch { /* 富化失败不阻塞 /models */ }
42
- }
43
- // 勾选即对外目录(?all=1 逃生门):picks 非空只暴露勾选项;空 picks 不过滤
44
- if (!/(^|&)all=1(&|$)/.test(qs)) {
45
- let picks = [];
46
- try {
47
- picks = (loadPicks ? await loadPicks() : (await import("../state.js")).loadModelPicks()) || [];
48
- } catch { picks = []; }
49
- data = filterByPicks(data, picks);
50
- }
51
- // 插件 hook:models:list — 返回数组可替换对外模型列表({object:"list",data:[...]} 或纯 id 数组)
52
- if (plugins?.length) {
53
- const ml = await runHook(plugins, "models:list", { data });
54
- for (const e of ml.errors) console.log(`plugin models:list error (${e.plugin}): ${e.error}`);
55
- if (ml.changed && Array.isArray(ml.value)) {
56
- const idsOnly = ml.value.every((x) => typeof x === "string");
57
- const out = idsOnly
58
- ? { object: "list", data: ml.value.map((id) => ({ id, object: "model", owned_by: "plugin" })) }
59
- : { object: "list", data: ml.value };
60
- return json(res, 200, withCodex(out));
61
- }
62
- }
63
- json(res, 200, withCodex(data));
64
- } catch (err) {
65
- json(res, 502, { error: errMsg(err) });
66
- }
67
- }
68
-
69
- export async function modelsStatusHandler({ res, models, auto }) {
70
- const statuses = auto?.statuses?.() || {};
71
- let ids = [];
72
- try {
73
- ids = (await models?.get?.())?.data?.map((m) => m.id) || [];
74
- } catch {}
75
- const seen = new Set();
76
- const data = [];
77
- for (const id of [...ids, ...Object.keys(statuses)]) {
78
- if (seen.has(id)) continue;
79
- seen.add(id);
80
- const e = statuses[id];
81
- const entry = typeof e === "number"
82
- ? { id, status: "error", at: e }
83
- : { id, status: e?.status || "normal", at: e?.at ?? null, code: e?.code ?? null };
84
- data.push(entry);
85
- }
86
- json(res, 200, { object: "list", data });
87
- }
88
-
89
- export async function providerModelsHandler({ req, res, models }) {
90
- if (!models) return json(res, 501, { error: "Models service not configured" });
91
- const url = req.url || "";
92
- const path = url.split("?")[0] || "";
93
- const m = path.match(/^\/v1\/providers\/([^/]+)\/models\/?$/);
94
- const pid = m ? decodeURIComponent(m[1]).toLowerCase() : "";
95
- if (!pid) return json(res, 400, { error: "missing provider id" });
96
- try {
97
- const data = await models.get();
98
- const all = Array.isArray(data?.data) ? data.data : [];
99
- const filtered = all.filter((entry) => {
100
- const id = String(entry?.id || "");
101
- const slash = id.indexOf("/");
102
- const prov = slash > 0 ? id.slice(0, slash).toLowerCase() : "opencode";
103
- if (prov !== pid) return false;
104
- const raw = slash > 0 ? id.slice(slash + 1) : id;
105
- // allowlist check: opencode always allowed via allowAny, others respect config
106
- try {
107
- return isModelAllowed(pid, raw);
108
- } catch { return true; }
109
- });
110
- json(res, 200, { object: "list", data: filtered });
111
- } catch (err) {
112
- json(res, 502, { error: errMsg(err) });
113
- }
114
- }
115
-
116
- // workbuddy 能力目录源:共享 provider 单例(src/model-capabilities/index.js,
117
- // 与 -setto opencode 能力注入共用),拉上游原生字段全量 + 10min 缓存
118
- // 不走聚合 models.get()——聚合会按 allowlist 过滤,blocked 模型查能力 404 会误导(能力目录≠可调用目录,与 CLI 全量展示保持一致)
119
- async function workbuddyAllModels() {
120
- const { workbuddyAllModels: wb } = await import("../model-capabilities/index.js");
121
- return wb();
122
- }
123
-
124
- // GET /v1/models/capabilities[?provider=opencode|workbuddy][&id=big-pickle]
125
- // 模型能力元数据(reasoning 档位/图片输入/tool_call/上下文/价格)
126
- // - provider=opencode(默认):源 = opencode 官方 models.dev 目录
127
- // - provider=workbuddy:源 = workbuddy 上游原生字段(maxInputTokens/supportsImages/... first-party 最准),
128
- // id 用裸 id(如 glm-5.3-flash);wbSource 注入仅测试接缝,生产用独立 provider 单例(全量+10min 缓存)
129
- // jsonFn 注入仅为测试接缝(S3);生产路径用 helpers.json
130
- export async function capabilitiesHandler({ req, res, capabilities, wbSource, jsonFn = json }) {
131
- // 仅未注入(生产)时用全局单例;显式 null 视作服务不可用(测试可复现 502 空状态)
132
- const svc = capabilities === undefined ? globalCapabilities() : capabilities;
133
- if (!svc && !wbSource) return jsonFn(res, 502, { error: "capabilities service unavailable" });
134
- const q = String(req?.url || "").split("?")[1] || "";
135
- const params = new URLSearchParams(q);
136
- const provider = (params.get("provider") || "opencode").toLowerCase();
137
- const id = params.get("id") || "";
138
- // workbuddy 动态源分支:上游原生字段映射,不查 models.dev
139
- if (provider === "workbuddy") {
140
- try {
141
- const { workbuddyCapsFromModels } = await import("../model-capabilities/index.js");
142
- const map = await workbuddyCapsFromModels(wbSource || workbuddyAllModels)();
143
- const rawId = id.replace(/^workbuddy\//i, "");
144
- if (id) {
145
- const caps = map[rawId];
146
- if (!caps) return jsonFn(res, 404, { error: `model '${id}' not found in capabilities catalog (provider=workbuddy)` });
147
- return jsonFn(res, 200, { object: "model.capabilities", id: rawId, provider, capabilities: caps });
148
- }
149
- const data = Object.entries(map).map(([mid, capabilities]) => ({ id: mid, capabilities }));
150
- return jsonFn(res, 200, { object: "list", provider, data });
151
- } catch (err) {
152
- return jsonFn(res, 502, { error: `capabilities source unavailable: ${errMsg(err)}` });
153
- }
154
- }
155
- try {
156
- await svc.ready();
157
- } catch (err) {
158
- return jsonFn(res, 502, { error: `capabilities source unavailable: ${errMsg(err)}` });
159
- }
160
- if (id) {
161
- const caps = svc.get(provider, id);
162
- if (!caps) return jsonFn(res, 404, { error: `model '${id}' not found in capabilities catalog (provider=${provider})` });
163
- return jsonFn(res, 200, { object: "model.capabilities", id, provider, capabilities: caps });
164
- }
165
- const data = svc.list(provider);
166
- return jsonFn(res, 200, { object: "list", provider, data });
167
- }
1
+ import { json, errMsg } from "./helpers.js";
2
+ import { runHook } from "../plugins.js";
3
+ import { isModelAllowed } from "../state.js";
4
+ import { mergeModelsList } from "../model-capabilities/merge.js";
5
+ import { globalCapabilities } from "../model-capabilities/index.js";
6
+
7
+ // Codex 自定义 provider 拉目录要顶层 `models` 数组(codex-rs endpoint/models.rs 解 ModelsResponse{models}),
8
+ // 给它 OpenAI 标准 {object,data} 会报 missing field `models`。学 OmniRoute:仅 codex 调用者追加空数组
9
+ // (填真目录反而会覆盖 codex 内置 agent prompt,必须空),其他客户端保持字节一致。
10
+ export function isCodexModelsCaller(req) {
11
+ const h = req?.headers || {};
12
+ if (/^codex_/i.test(String(h["user-agent"] || ""))) return true;
13
+ if (/^codex_/i.test(String(h.originator || ""))) return true;
14
+ const q = String(req?.url || "").split("?")[1] || "";
15
+ return /(^|&)client_version=/.test(q);
16
+ }
17
+
18
+ // 勾选即对外目录:modelPicks 非空时 /v1/models 只暴露勾选的模型(与 -models 交互勾选语义闭环);
19
+ // 空勾选 = 不过滤(保留旧默认全量目录,防呆:新装/清空勾选不至于对外空目录)。?all=1 逃生门。
20
+ // Note: 空勾选不裁剪 + ?all=1 逃生门是硬边界(缺一即成单向棘轮/下游零模型)— 见 .agents/notes/implemented/architecture/2026-09-21-models-list-scoped-by-picks.md
21
+ export function filterByPicks(data, picks) {
22
+ if (!picks?.length) return data;
23
+ const set = new Set(picks.map(String));
24
+ const list = (Array.isArray(data?.data) ? data.data : []).filter((m) => set.has(String(m?.id || "")));
25
+ return { ...(data || { object: "list" }), data: list };
26
+ }
27
+
28
+ export async function modelsHandler({ req, res, models, plugins, capabilities, wbSource, loadPicks } = {}) {
29
+ if (!models) return json(res, 501, { error: "Models service not configured" });
30
+ const codex = isCodexModelsCaller(req);
31
+ const withCodex = (out) => (codex && out && typeof out === "object" ? { ...out, models: [] } : out);
32
+ try {
33
+ let data = await models.get();
34
+ // ADR-0022:默认把能力富化进每条条目(capabilities 子对象);?raw=1 逃生门回原始形状,
35
+ // codex 调用者保持原始 + 顶层空 models:[](富化白做)。best-effort,失败原样返回。
36
+ const qs = String(req?.url || "").split("?")[1] || "";
37
+ const rawMode = /(^|&)raw=1(&|$)/.test(qs);
38
+ if (!codex && !rawMode) {
39
+ try {
40
+ data = await mergeModelsList(data, { capsSvc: capabilities, wbSource });
41
+ } catch { /* 富化失败不阻塞 /models */ }
42
+ }
43
+ // 勾选即对外目录(?all=1 逃生门):picks 非空只暴露勾选项;空 picks 不过滤
44
+ if (!/(^|&)all=1(&|$)/.test(qs)) {
45
+ let picks = [];
46
+ try {
47
+ picks = (loadPicks ? await loadPicks() : (await import("../state.js")).loadModelPicks()) || [];
48
+ } catch { picks = []; }
49
+ data = filterByPicks(data, picks);
50
+ }
51
+ // 插件 hook:models:list — 返回数组可替换对外模型列表({object:"list",data:[...]} 或纯 id 数组)
52
+ if (plugins?.length) {
53
+ const ml = await runHook(plugins, "models:list", { data });
54
+ for (const e of ml.errors) console.log(`plugin models:list error (${e.plugin}): ${e.error}`);
55
+ if (ml.changed && Array.isArray(ml.value)) {
56
+ const idsOnly = ml.value.every((x) => typeof x === "string");
57
+ const out = idsOnly
58
+ ? { object: "list", data: ml.value.map((id) => ({ id, object: "model", owned_by: "plugin" })) }
59
+ : { object: "list", data: ml.value };
60
+ return json(res, 200, withCodex(out));
61
+ }
62
+ }
63
+ json(res, 200, withCodex(data));
64
+ } catch (err) {
65
+ json(res, 502, { error: errMsg(err) });
66
+ }
67
+ }
68
+
69
+ export async function modelsStatusHandler({ res, models, auto }) {
70
+ const statuses = auto?.statuses?.() || {};
71
+ let ids = [];
72
+ try {
73
+ ids = (await models?.get?.())?.data?.map((m) => m.id) || [];
74
+ } catch {}
75
+ const seen = new Set();
76
+ const data = [];
77
+ for (const id of [...ids, ...Object.keys(statuses)]) {
78
+ if (seen.has(id)) continue;
79
+ seen.add(id);
80
+ const e = statuses[id];
81
+ const entry = typeof e === "number"
82
+ ? { id, status: "error", at: e }
83
+ : { id, status: e?.status || "normal", at: e?.at ?? null, code: e?.code ?? null };
84
+ data.push(entry);
85
+ }
86
+ json(res, 200, { object: "list", data });
87
+ }
88
+
89
+ export async function providerModelsHandler({ req, res, models }) {
90
+ if (!models) return json(res, 501, { error: "Models service not configured" });
91
+ const url = req.url || "";
92
+ const path = url.split("?")[0] || "";
93
+ const m = path.match(/^\/v1\/providers\/([^/]+)\/models\/?$/);
94
+ const pid = m ? decodeURIComponent(m[1]).toLowerCase() : "";
95
+ if (!pid) return json(res, 400, { error: "missing provider id" });
96
+ try {
97
+ const data = await models.get();
98
+ const all = Array.isArray(data?.data) ? data.data : [];
99
+ const filtered = all.filter((entry) => {
100
+ const id = String(entry?.id || "");
101
+ const slash = id.indexOf("/");
102
+ const prov = slash > 0 ? id.slice(0, slash).toLowerCase() : "opencode";
103
+ if (prov !== pid) return false;
104
+ const raw = slash > 0 ? id.slice(slash + 1) : id;
105
+ // allowlist check: opencode always allowed via allowAny, others respect config
106
+ try {
107
+ return isModelAllowed(pid, raw);
108
+ } catch { return true; }
109
+ });
110
+ json(res, 200, { object: "list", data: filtered });
111
+ } catch (err) {
112
+ json(res, 502, { error: errMsg(err) });
113
+ }
114
+ }
115
+
116
+ // workbuddy 能力目录源:共享 provider 单例(src/model-capabilities/index.js,
117
+ // 与 -setto opencode 能力注入共用),拉上游原生字段全量 + 10min 缓存
118
+ // 不走聚合 models.get()——聚合会按 allowlist 过滤,blocked 模型查能力 404 会误导(能力目录≠可调用目录,与 CLI 全量展示保持一致)
119
+ async function workbuddyAllModels() {
120
+ const { workbuddyAllModels: wb } = await import("../model-capabilities/index.js");
121
+ return wb();
122
+ }
123
+
124
+ // GET /v1/models/capabilities[?provider=opencode|workbuddy][&id=big-pickle]
125
+ // 模型能力元数据(reasoning 档位/图片输入/tool_call/上下文/价格)
126
+ // - provider=opencode(默认):源 = opencode 官方 models.dev 目录
127
+ // - provider=workbuddy:源 = workbuddy 上游原生字段(maxInputTokens/supportsImages/... first-party 最准),
128
+ // id 用裸 id(如 glm-5.3-flash);wbSource 注入仅测试接缝,生产用独立 provider 单例(全量+10min 缓存)
129
+ // jsonFn 注入仅为测试接缝(S3);生产路径用 helpers.json
130
+ export async function capabilitiesHandler({ req, res, capabilities, wbSource, jsonFn = json }) {
131
+ // 仅未注入(生产)时用全局单例;显式 null 视作服务不可用(测试可复现 502 空状态)
132
+ const svc = capabilities === undefined ? globalCapabilities() : capabilities;
133
+ if (!svc && !wbSource) return jsonFn(res, 502, { error: "capabilities service unavailable" });
134
+ const q = String(req?.url || "").split("?")[1] || "";
135
+ const params = new URLSearchParams(q);
136
+ const provider = (params.get("provider") || "opencode").toLowerCase();
137
+ const id = params.get("id") || "";
138
+ // workbuddy 动态源分支:上游原生字段映射,不查 models.dev
139
+ if (provider === "workbuddy") {
140
+ try {
141
+ const { workbuddyCapsFromModels } = await import("../model-capabilities/index.js");
142
+ const map = await workbuddyCapsFromModels(wbSource || workbuddyAllModels)();
143
+ const rawId = id.replace(/^workbuddy\//i, "");
144
+ if (id) {
145
+ const caps = map[rawId];
146
+ if (!caps) return jsonFn(res, 404, { error: `model '${id}' not found in capabilities catalog (provider=workbuddy)` });
147
+ return jsonFn(res, 200, { object: "model.capabilities", id: rawId, provider, capabilities: caps });
148
+ }
149
+ const data = Object.entries(map).map(([mid, capabilities]) => ({ id: mid, capabilities }));
150
+ return jsonFn(res, 200, { object: "list", provider, data });
151
+ } catch (err) {
152
+ return jsonFn(res, 502, { error: `capabilities source unavailable: ${errMsg(err)}` });
153
+ }
154
+ }
155
+ try {
156
+ await svc.ready();
157
+ } catch (err) {
158
+ return jsonFn(res, 502, { error: `capabilities source unavailable: ${errMsg(err)}` });
159
+ }
160
+ if (id) {
161
+ const caps = svc.get(provider, id);
162
+ if (!caps) return jsonFn(res, 404, { error: `model '${id}' not found in capabilities catalog (provider=${provider})` });
163
+ return jsonFn(res, 200, { object: "model.capabilities", id, provider, capabilities: caps });
164
+ }
165
+ const data = svc.list(provider);
166
+ return jsonFn(res, 200, { object: "list", provider, data });
167
+ }