mslxdff 0.1.197 → 0.1.199

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 (95) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/docs/cli_help_mini.md +146 -0
  3. package/package.json +1 -1
  4. package/src/auto.js +254 -254
  5. package/src/bench/cline-bench.js +42 -42
  6. package/src/bench/probe.js +70 -70
  7. package/src/bench/report.js +162 -162
  8. package/src/bench/runner.js +77 -77
  9. package/src/bench/via-probe.js +124 -124
  10. package/src/bench/via-routes.js +87 -87
  11. package/src/bench/workbuddy-bench.js +54 -54
  12. package/src/chat/engine.js +160 -160
  13. package/src/chat/gateway.js +163 -163
  14. package/src/chat/orchestrator.js +234 -234
  15. package/src/chat/prompt.js +70 -70
  16. package/src/chat/repl.js +88 -88
  17. package/src/chat/terminal.js +135 -135
  18. package/src/chat/tools.js +306 -306
  19. package/src/chat-pipeline/index.js +173 -173
  20. package/src/chat-pipeline/policy.js +84 -84
  21. package/src/chat-pipeline/serial-trial.js +8 -6
  22. package/src/cli/commands/group.js +249 -249
  23. package/src/cli/commands/model/picks.js +50 -50
  24. package/src/cli/commands/provider/bench-via.js +247 -247
  25. package/src/cli/commands/provider/bench.js +141 -141
  26. package/src/cli/commands/provider/index.js +137 -136
  27. package/src/cli/commands/provider/models.js +145 -145
  28. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  29. package/src/cli/commands/provider/raccoon-login.js +29 -1
  30. package/src/cli/commands/provider/raccoon-password.js +162 -0
  31. package/src/cli/commands/sync.js +310 -310
  32. package/src/cli/status.js +279 -279
  33. package/src/daemon.js +232 -232
  34. package/src/model-capabilities/enrich.js +86 -86
  35. package/src/model-capabilities/index.js +183 -183
  36. package/src/model-capabilities/parse.js +70 -70
  37. package/src/models.js +225 -225
  38. package/src/providers/AGENTS.md +49 -0
  39. package/src/providers/cline/auth.js +228 -228
  40. package/src/providers/cline/chat.js +307 -307
  41. package/src/providers/cline.js +2 -2
  42. package/src/providers/codearts/chat.js +182 -182
  43. package/src/providers/globalqwenwork/cosy.js +123 -123
  44. package/src/providers/globalqwenwork/crypto.js +218 -218
  45. package/src/providers/globalqwenwork/http.js +20 -20
  46. package/src/providers/globalqwenwork/rsa.js +54 -54
  47. package/src/providers/globalqwenwork/sse.js +268 -268
  48. package/src/providers/globalqwenwork/stream.js +130 -130
  49. package/src/providers/globalqwenwork/upstream.js +122 -122
  50. package/src/providers/keyring.js +60 -60
  51. package/src/providers/qoder/chat.js +203 -203
  52. package/src/providers/qoder/index.js +249 -249
  53. package/src/providers/qoder/sse.js +145 -145
  54. package/src/providers/qwenwork/account-store.js +133 -133
  55. package/src/providers/qwenwork/constants.js +67 -67
  56. package/src/providers/qwenwork/cosy.js +120 -120
  57. package/src/providers/qwenwork/crypto.js +218 -218
  58. package/src/providers/qwenwork/http.js +20 -20
  59. package/src/providers/qwenwork/index.js +337 -337
  60. package/src/providers/qwenwork/payload.js +144 -144
  61. package/src/providers/qwenwork/rsa.js +54 -54
  62. package/src/providers/qwenwork/sse.js +268 -268
  63. package/src/providers/qwenwork/stream.js +130 -130
  64. package/src/providers/qwenwork/upstream.js +120 -120
  65. package/src/providers/qwenwork.js +1 -1
  66. package/src/providers/raccoon/const.js +25 -0
  67. package/src/providers/raccoon/password-login.js +87 -0
  68. package/src/providers/registry.js +79 -79
  69. package/src/providers/workbuddy/chat.js +264 -264
  70. package/src/providers/workbuddy/reshape.js +152 -152
  71. package/src/providers/workbuddy.js +2 -2
  72. package/src/reasoning.js +32 -32
  73. package/src/routes/AGENTS.md +37 -0
  74. package/src/routes/chat/gateway.js +46 -46
  75. package/src/routes/chat/relay-pipeline.js +282 -282
  76. package/src/routes/chat/via-route-handler.js +146 -146
  77. package/src/routes/hedge.js +260 -260
  78. package/src/routes/models-route.js +167 -167
  79. package/src/routes/peers.js +335 -335
  80. package/src/routes/stream.js +347 -347
  81. package/src/runtime/bootstrap.js +67 -67
  82. package/src/runtime/provider-gate.js +36 -36
  83. package/src/runtime/providers-setup.js +168 -168
  84. package/src/server.js +78 -78
  85. package/src/state/schemas/allowlist.js +92 -92
  86. package/src/sync-opencode.js +280 -280
  87. package/src/transport/index.js +244 -244
  88. package/src/transport/pool.js +56 -56
  89. package/src/transport/retry.js +24 -24
  90. package/src/transport/sse.js +93 -93
  91. package/src/upstream-probe/display.js +52 -52
  92. package/src/upstream-probe/probe.js +49 -49
  93. package/src/upstream-probe/rotate.js +110 -110
  94. package/src/upstream-probe/start.js +45 -45
  95. package/src/upstream.js +293 -293
@@ -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
+ }