mslxdff 0.1.231 → 0.1.232

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 (104) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +254 -254
  4. package/src/bench/cline-bench.js +42 -42
  5. package/src/bench/probe.js +70 -70
  6. package/src/bench/report.js +162 -162
  7. package/src/bench/runner.js +77 -77
  8. package/src/bench/via-probe.js +124 -124
  9. package/src/bench/via-routes.js +87 -87
  10. package/src/bench/workbuddy-bench.js +54 -54
  11. package/src/chat/engine.js +160 -160
  12. package/src/chat/gateway.js +163 -163
  13. package/src/chat/orchestrator.js +234 -234
  14. package/src/chat/prompt.js +70 -70
  15. package/src/chat/repl.js +88 -88
  16. package/src/chat/terminal.js +135 -135
  17. package/src/chat/tools.js +306 -306
  18. package/src/chat-pipeline/index.js +145 -145
  19. package/src/chat-pipeline/policy.js +84 -84
  20. package/src/cli/commands/group.js +266 -266
  21. package/src/cli/commands/model/picks.js +50 -50
  22. package/src/cli/commands/provider/bench-via.js +247 -247
  23. package/src/cli/commands/provider/bench.js +141 -141
  24. package/src/cli/commands/provider/index.js +139 -139
  25. package/src/cli/commands/provider/models.js +149 -149
  26. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  27. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  28. package/src/cli/commands/sync.js +310 -310
  29. package/src/cli/status.js +279 -279
  30. package/src/daemon.js +246 -246
  31. package/src/model-capabilities/enrich.js +86 -86
  32. package/src/model-capabilities/index.js +183 -183
  33. package/src/model-capabilities/parse.js +70 -70
  34. package/src/models.js +225 -225
  35. package/src/peers.js +345 -345
  36. package/src/providers/agentrouter.js +238 -238
  37. package/src/providers/base.js +164 -164
  38. package/src/providers/cline/auth.js +228 -228
  39. package/src/providers/cline/chat.js +307 -307
  40. package/src/providers/cline.js +2 -2
  41. package/src/providers/codearts/chat.js +207 -207
  42. package/src/providers/dispatcher.js +147 -147
  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 +202 -202
  67. package/src/providers/raccoon/credits.js +285 -285
  68. package/src/providers/registry.js +89 -89
  69. package/src/providers/workbuddy/auth.js +16 -0
  70. package/src/providers/workbuddy/chat.js +298 -264
  71. package/src/providers/workbuddy/client-headers.js +148 -0
  72. package/src/providers/workbuddy/desktop-info.js +17 -1
  73. package/src/providers/workbuddy/models.js +8 -20
  74. package/src/providers/workbuddy/reshape.js +152 -152
  75. package/src/providers/workbuddy.js +2 -2
  76. package/src/reasoning.js +32 -32
  77. package/src/routes/chat/broadband-handler.js +80 -80
  78. package/src/routes/chat/gateway.js +46 -46
  79. package/src/routes/chat/relay-pipeline.js +282 -282
  80. package/src/routes/chat/via-route-handler.js +156 -156
  81. package/src/routes/hedge.js +260 -260
  82. package/src/routes/models-route.js +167 -167
  83. package/src/routes/peers.js +373 -373
  84. package/src/routes/stream.js +347 -347
  85. package/src/runtime/bootstrap.js +75 -75
  86. package/src/runtime/keep-warm.js +135 -135
  87. package/src/runtime/provider-gate.js +75 -75
  88. package/src/runtime/providers-setup.js +187 -187
  89. package/src/runtime/raccoon-checkin.js +175 -175
  90. package/src/server.js +85 -85
  91. package/src/state/schemas/allowlist.js +92 -92
  92. package/src/sync-opencode.js +280 -280
  93. package/src/transport/index.js +244 -244
  94. package/src/transport/pool.js +56 -56
  95. package/src/transport/retry.js +24 -24
  96. package/src/transport/sse.js +93 -93
  97. package/src/upstream-probe/display.js +52 -52
  98. package/src/upstream-probe/probe.js +49 -49
  99. package/src/upstream-probe/rotate.js +110 -110
  100. package/src/upstream-probe/start.js +45 -45
  101. package/src/upstream.js +355 -355
  102. package/docs/cli_help_mini.md +0 -148
  103. package/src/providers/AGENTS.md +0 -49
  104. package/src/routes/AGENTS.md +0 -37
@@ -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
+ }