minecodex 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/features/model-slider/README.md +7 -4
- package/features/model-slider/codex-feature.json +0 -7
- package/features/notes/src/http-server.mjs +3 -0
- package/features/notes/web/app.js +9 -303
- package/features/notes/web/file-icons.mjs +68 -0
- package/features/notes/web/i18n.mjs +237 -0
- package/features/notes/web/todo-drag.mjs +18 -0
- package/package.json +1 -1
- package/packages/runtime-host/src/codex-cdp.mjs +153 -0
- package/packages/runtime-host/src/codex-design-contract.mjs +116 -0
- package/packages/runtime-host/src/codex-efforts.mjs +47 -0
- package/packages/runtime-host/src/codex-injection.mjs +4545 -0
- package/packages/runtime-host/src/codex-runtime.mjs +63 -4930
- package/packages/runtime-host/src/feature-registry.mjs +1 -13
- package/packages/runtime-host/src/host-actions.mjs +221 -0
- package/packages/runtime-host/src/main.mjs +7 -9
- package/packages/runtime-host/src/model-capabilities.mjs +0 -86
|
@@ -120,18 +120,6 @@ async function normalizeModelSelector(modelSelector, projectDir) {
|
|
|
120
120
|
throw new Error("modelSelector.favoriteStorageKey is required");
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
const reasoningEffortOverrides = {};
|
|
124
|
-
for (const [identity, efforts] of Object.entries(modelSelector.reasoningEffortOverrides ?? {})) {
|
|
125
|
-
const exactIdentity = String(identity).trim().toLowerCase();
|
|
126
|
-
const allowedEfforts = Array.isArray(efforts)
|
|
127
|
-
? efforts.map((effort) => String(effort).trim().toLowerCase()).filter(Boolean)
|
|
128
|
-
: [];
|
|
129
|
-
if (!exactIdentity || !allowedEfforts.length || new Set(allowedEfforts).size !== allowedEfforts.length) {
|
|
130
|
-
throw new Error("modelSelector.reasoningEffortOverrides must map exact identities to unique efforts");
|
|
131
|
-
}
|
|
132
|
-
reasoningEffortOverrides[exactIdentity] = allowedEfforts;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
123
|
const providerAliases = {};
|
|
136
124
|
for (const [provider, label] of Object.entries(modelSelector.providerAliases ?? {})) {
|
|
137
125
|
if (!String(provider).trim() || !String(label).trim()) {
|
|
@@ -165,7 +153,6 @@ async function normalizeModelSelector(modelSelector, projectDir) {
|
|
|
165
153
|
return {
|
|
166
154
|
maxVisibleItems,
|
|
167
155
|
favoriteStorageKey,
|
|
168
|
-
reasoningEffortOverrides,
|
|
169
156
|
providerAliases,
|
|
170
157
|
strings,
|
|
171
158
|
icons: {
|
|
@@ -367,6 +354,7 @@ export async function loadConfiguredModelCatalog(
|
|
|
367
354
|
].every((value) => typeof value === "string" && value.trim())
|
|
368
355
|
? `${model.opencodex_capability_provenance.provider.trim()}/${model.opencodex_capability_provenance.model_id.trim()}`
|
|
369
356
|
: null,
|
|
357
|
+
opencodex_capability_provenance: model.opencodex_capability_provenance ?? null,
|
|
370
358
|
})).filter((model) => model.slug);
|
|
371
359
|
} catch (error) {
|
|
372
360
|
if (error.code === "ENOENT") return [];
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Host action 命令:Codex Composer 的文件附件、图片编辑线程与降级路径。
|
|
2
|
+
// 每个命令是参数注入的纯函数(client 与依赖由 CodexRuntime 传入),
|
|
3
|
+
// CodexRuntime 只做 handleBindingCalled 路由,不再持有命令实现。
|
|
4
|
+
|
|
5
|
+
import { stat } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { CODEX_APP_ORIGIN } from "./codex-injection.mjs";
|
|
8
|
+
import {
|
|
9
|
+
evaluationValue,
|
|
10
|
+
waitForExpression,
|
|
11
|
+
} from "./codex-cdp.mjs";
|
|
12
|
+
export async function attachFileToComposer(client, filePath, requestId) {
|
|
13
|
+
if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
|
|
14
|
+
throw new Error("An absolute file path is required");
|
|
15
|
+
}
|
|
16
|
+
const info = await stat(filePath);
|
|
17
|
+
if (!info.isFile()) throw new Error("Only regular files can be attached");
|
|
18
|
+
const fileName = path.basename(filePath);
|
|
19
|
+
const marker = `codex-personal-${requestId}`;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
evaluationValue(await client.send("Runtime.evaluate", {
|
|
23
|
+
expression: `(() => {
|
|
24
|
+
document.querySelectorAll('[data-codex-personal-file-input]').forEach((input) => input.remove());
|
|
25
|
+
const input = document.createElement("input");
|
|
26
|
+
input.type = "file";
|
|
27
|
+
input.multiple = true;
|
|
28
|
+
input.dataset.codexPersonalFileInput = ${JSON.stringify(marker)};
|
|
29
|
+
input.style.display = "none";
|
|
30
|
+
document.body.append(input);
|
|
31
|
+
return true;
|
|
32
|
+
})()`,
|
|
33
|
+
returnByValue: true,
|
|
34
|
+
}));
|
|
35
|
+
const { root } = await client.send("DOM.getDocument", { depth: 0 });
|
|
36
|
+
const { nodeId } = await client.send("DOM.querySelector", {
|
|
37
|
+
nodeId: root.nodeId,
|
|
38
|
+
selector: `[data-codex-personal-file-input="${marker}"]`,
|
|
39
|
+
});
|
|
40
|
+
if (!nodeId) throw new Error("The temporary file bridge could not be resolved");
|
|
41
|
+
await client.send("DOM.setFileInputFiles", { files: [filePath], nodeId });
|
|
42
|
+
|
|
43
|
+
const attachment = evaluationValue(await client.send("Runtime.evaluate", {
|
|
44
|
+
expression: `(async () => {
|
|
45
|
+
const marker = ${JSON.stringify(marker)};
|
|
46
|
+
const fileName = ${JSON.stringify(fileName)};
|
|
47
|
+
const input = Array.from(document.querySelectorAll('[data-codex-personal-file-input]'))
|
|
48
|
+
.find((candidate) => candidate.dataset.codexPersonalFileInput === marker);
|
|
49
|
+
const composer = document.querySelector('[data-codex-composer="true"][contenteditable="true"]');
|
|
50
|
+
if (!input?.files?.length || !composer) return { attached: false, reason: "Composer unavailable" };
|
|
51
|
+
const transfer = new DataTransfer();
|
|
52
|
+
for (const file of input.files) transfer.items.add(file);
|
|
53
|
+
for (const type of ["dragenter", "dragover", "drop"]) {
|
|
54
|
+
composer.dispatchEvent(new DragEvent(type, {
|
|
55
|
+
bubbles: true,
|
|
56
|
+
cancelable: true,
|
|
57
|
+
composed: true,
|
|
58
|
+
dataTransfer: transfer,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 850));
|
|
62
|
+
const attached = Boolean(document.querySelector(
|
|
63
|
+
'button[aria-label=' + JSON.stringify('Remove ' + fileName) + ']'
|
|
64
|
+
));
|
|
65
|
+
return { attached, reason: attached ? null : "Native attachment chip was not observed" };
|
|
66
|
+
})()`,
|
|
67
|
+
awaitPromise: true,
|
|
68
|
+
returnByValue: true,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
if (attachment?.attached) return { mode: "attach", name: fileName };
|
|
72
|
+
const fallback = evaluationValue(await client.send("Runtime.evaluate", {
|
|
73
|
+
expression: `window.__codexPersonalRuntime?.insertText(${JSON.stringify(filePath)})`,
|
|
74
|
+
returnByValue: true,
|
|
75
|
+
}));
|
|
76
|
+
if (!fallback) throw new Error(attachment?.reason ?? "File attachment failed");
|
|
77
|
+
return { mode: "insert-path", path: filePath, fallback: true };
|
|
78
|
+
} finally {
|
|
79
|
+
await client.send("Runtime.evaluate", {
|
|
80
|
+
expression: `document.querySelectorAll('[data-codex-personal-file-input]').forEach((input) => input.remove())`,
|
|
81
|
+
}).catch(() => {});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function attachFilesToComposer(client, filePaths, requestId) {
|
|
86
|
+
if (!Array.isArray(filePaths) || !filePaths.length || filePaths.length > 2) {
|
|
87
|
+
throw new Error("One or two image edit attachments are required");
|
|
88
|
+
}
|
|
89
|
+
for (const filePath of filePaths) {
|
|
90
|
+
if (typeof filePath !== "string" || !path.isAbsolute(filePath) || !(await stat(filePath)).isFile()) {
|
|
91
|
+
throw new Error("Only absolute regular files can be attached");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const fileNames = filePaths.map((filePath) => path.basename(filePath));
|
|
95
|
+
const requiredLabels = fileNames.reduce((counts, fileName) => {
|
|
96
|
+
const label = `Remove ${fileName}`;
|
|
97
|
+
counts[label] = (counts[label] ?? 0) + 1;
|
|
98
|
+
return counts;
|
|
99
|
+
}, {});
|
|
100
|
+
const marker = `codex-personal-${requestId}-batch`;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
evaluationValue(await client.send("Runtime.evaluate", {
|
|
104
|
+
expression: `(() => {
|
|
105
|
+
document.querySelectorAll('[data-codex-personal-file-input]').forEach((input) => input.remove());
|
|
106
|
+
const input = document.createElement("input");
|
|
107
|
+
input.type = "file";
|
|
108
|
+
input.multiple = true;
|
|
109
|
+
input.dataset.codexPersonalFileInput = ${JSON.stringify(marker)};
|
|
110
|
+
input.style.display = "none";
|
|
111
|
+
document.body.append(input);
|
|
112
|
+
return true;
|
|
113
|
+
})()`,
|
|
114
|
+
returnByValue: true,
|
|
115
|
+
}));
|
|
116
|
+
const { root } = await client.send("DOM.getDocument", { depth: 0 });
|
|
117
|
+
const { nodeId } = await client.send("DOM.querySelector", {
|
|
118
|
+
nodeId: root.nodeId,
|
|
119
|
+
selector: `[data-codex-personal-file-input="${marker}"]`,
|
|
120
|
+
});
|
|
121
|
+
if (!nodeId) throw new Error("The temporary multi-file bridge could not be resolved");
|
|
122
|
+
await client.send("DOM.setFileInputFiles", { files: filePaths, nodeId });
|
|
123
|
+
const dropped = evaluationValue(await client.send("Runtime.evaluate", {
|
|
124
|
+
expression: `(() => {
|
|
125
|
+
const input = document.querySelector(${JSON.stringify(`[data-codex-personal-file-input="${marker}"]`)});
|
|
126
|
+
const composer = document.querySelector('[data-codex-composer="true"][contenteditable="true"]');
|
|
127
|
+
if (!input?.files?.length || !composer) return false;
|
|
128
|
+
const transfer = new DataTransfer();
|
|
129
|
+
for (const file of input.files) transfer.items.add(file);
|
|
130
|
+
for (const type of ["dragenter", "dragover", "drop"]) {
|
|
131
|
+
composer.dispatchEvent(new DragEvent(type, {
|
|
132
|
+
bubbles: true,
|
|
133
|
+
cancelable: true,
|
|
134
|
+
composed: true,
|
|
135
|
+
dataTransfer: transfer,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
return transfer.files.length === ${filePaths.length};
|
|
139
|
+
})()`,
|
|
140
|
+
returnByValue: true,
|
|
141
|
+
}));
|
|
142
|
+
if (!dropped) throw new Error("The native Composer rejected the image edit drop");
|
|
143
|
+
await waitForExpression(client, `(() => {
|
|
144
|
+
const required = ${JSON.stringify(requiredLabels)};
|
|
145
|
+
const observed = {};
|
|
146
|
+
for (const button of document.querySelectorAll('button[aria-label^="Remove "]')) {
|
|
147
|
+
const label = button.getAttribute("aria-label");
|
|
148
|
+
observed[label] = (observed[label] ?? 0) + 1;
|
|
149
|
+
}
|
|
150
|
+
return Object.entries(required).every(([label, count]) => (observed[label] ?? 0) >= count);
|
|
151
|
+
})()`, 10_000);
|
|
152
|
+
return fileNames.map((name) => ({ mode: "attach", name }));
|
|
153
|
+
} finally {
|
|
154
|
+
await client.send("Runtime.evaluate", {
|
|
155
|
+
expression: `document.querySelectorAll('[data-codex-personal-file-input]').forEach((input) => input.remove())`,
|
|
156
|
+
}).catch(() => {});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function createImageEditThread(client, feature, payload, requestId, { fetchImpl, attachFiles = attachFilesToComposer } = {}) {
|
|
161
|
+
const draftId = typeof payload?.draftId === "string" ? payload.draftId : "";
|
|
162
|
+
const prompt = typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
|
|
163
|
+
if (!/^[a-f0-9-]{36}$/.test(draftId) || !prompt || prompt.length > 2_000 || !feature?.surfaceUrl) {
|
|
164
|
+
throw Object.assign(new Error("The image edit request is incomplete"), { code: "INVALID_EDIT_REQUEST" });
|
|
165
|
+
}
|
|
166
|
+
const draftResponse = await fetchImpl(new URL(`/api/image-edit-draft/${draftId}`, feature.surfaceUrl), {
|
|
167
|
+
headers: { Origin: CODEX_APP_ORIGIN },
|
|
168
|
+
});
|
|
169
|
+
if (!draftResponse?.ok) {
|
|
170
|
+
throw Object.assign(new Error("The image edit draft is unavailable"), { code: "EDIT_DRAFT_UNAVAILABLE" });
|
|
171
|
+
}
|
|
172
|
+
const draft = await draftResponse.json();
|
|
173
|
+
const paths = [draft?.sourcePath, draft?.auxiliaryPath].filter(Boolean);
|
|
174
|
+
if (!paths.length || paths.length > 2) {
|
|
175
|
+
throw Object.assign(new Error("The image edit attachments are unavailable"), { code: "INVALID_EDIT_ATTACHMENT" });
|
|
176
|
+
}
|
|
177
|
+
for (const filePath of paths) {
|
|
178
|
+
if (typeof filePath !== "string" || !path.isAbsolute(filePath) || !(await stat(filePath)).isFile()) {
|
|
179
|
+
throw Object.assign(new Error("The image edit attachment is unavailable"), { code: "INVALID_EDIT_ATTACHMENT" });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const opened = evaluationValue(await client.send("Runtime.evaluate", {
|
|
184
|
+
expression: `(() => {
|
|
185
|
+
const candidates = Array.from(document.querySelectorAll('button, [role="button"]'));
|
|
186
|
+
const trigger = candidates.find((element) => /^new chat$/i.test(
|
|
187
|
+
(element.getAttribute('aria-label') || element.textContent || '').trim(),
|
|
188
|
+
));
|
|
189
|
+
if (!trigger) return false;
|
|
190
|
+
trigger.click();
|
|
191
|
+
return true;
|
|
192
|
+
})()`,
|
|
193
|
+
returnByValue: true,
|
|
194
|
+
}));
|
|
195
|
+
if (!opened) throw Object.assign(new Error("The native New chat control is unavailable"), {
|
|
196
|
+
code: "NEW_CHAT_UNAVAILABLE",
|
|
197
|
+
});
|
|
198
|
+
await waitForExpression(client, `Boolean(document.querySelector('[data-testid="home-icon"]')) && Boolean(document.querySelector('[data-codex-composer="true"][contenteditable="true"]'))`);
|
|
199
|
+
const attached = await attachFiles(client, paths, requestId);
|
|
200
|
+
evaluationValue(await client.send("Runtime.evaluate", {
|
|
201
|
+
expression: `window.__codexPersonalRuntime?.insertText(${JSON.stringify(prompt)})`,
|
|
202
|
+
returnByValue: true,
|
|
203
|
+
}));
|
|
204
|
+
const sent = evaluationValue(await client.send("Runtime.evaluate", {
|
|
205
|
+
expression: `(() => {
|
|
206
|
+
const send = document.querySelector('button[aria-label="Send message"]')
|
|
207
|
+
|| document.querySelector('button[aria-label="Send"]')
|
|
208
|
+
|| document.querySelector('button[data-testid="send-button"]');
|
|
209
|
+
if (!send || send.disabled) return false;
|
|
210
|
+
send.click();
|
|
211
|
+
return true;
|
|
212
|
+
})()`,
|
|
213
|
+
returnByValue: true,
|
|
214
|
+
}));
|
|
215
|
+
if (!sent) throw Object.assign(new Error("The new Chat is ready but its Send button is unavailable"), {
|
|
216
|
+
code: "SEND_UNAVAILABLE",
|
|
217
|
+
});
|
|
218
|
+
await waitForExpression(client, `!document.querySelector('[data-testid="home-icon"]') && Boolean(document.querySelector('[data-codex-composer="true"][contenteditable="true"]'))`);
|
|
219
|
+
return { attached, threadStarted: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
selectEnabledFeatures,
|
|
12
12
|
startFeatureProcesses,
|
|
13
13
|
} from "./feature-registry.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import { loadOpenCodexRealEfforts } from "./codex-efforts.mjs";
|
|
15
15
|
|
|
16
16
|
const runtimeRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
17
17
|
const featuresRoot = process.env.CODEX_FEATURES_ROOT ?? path.dirname(runtimeRoot);
|
|
@@ -23,16 +23,14 @@ const discoveredFeatures = await discoverFeatures(featuresRoot);
|
|
|
23
23
|
if (!discoveredFeatures.length) throw new Error(`No codex-feature.json files found under ${featuresRoot}`);
|
|
24
24
|
const features = selectEnabledFeatures(discoveredFeatures, process.env.MINECODEX_ENABLED_FEATURES);
|
|
25
25
|
const modelCatalog = await loadConfiguredModelCatalog();
|
|
26
|
-
|
|
26
|
+
// 以 OpenCodex 配置声明的真实档位为准,避免 catalog 里 mock 出的 max/ultra
|
|
27
|
+
// 出现在 Model Slider 中。后续新增模型无需改这里:在 OpenCodex 配置的
|
|
28
|
+
// providers.<id>.modelReasoningEfforts 里补一行即可,启动后自动生效。
|
|
29
|
+
const openCodexRealEfforts = await loadOpenCodexRealEfforts();
|
|
27
30
|
for (const feature of features) {
|
|
28
31
|
if (feature.modelSelector) {
|
|
29
|
-
feature.modelSelector.models = modelCatalog
|
|
30
|
-
|
|
31
|
-
model,
|
|
32
|
-
modelCapabilities.get(String(model.capabilityKey ?? model.slug ?? "").toLowerCase()),
|
|
33
|
-
model.supportedReasoningLevels,
|
|
34
|
-
)
|
|
35
|
-
));
|
|
32
|
+
feature.modelSelector.models = modelCatalog;
|
|
33
|
+
feature.modelSelector.openCodexRealEfforts = openCodexRealEfforts;
|
|
36
34
|
}
|
|
37
35
|
}
|
|
38
36
|
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
const CODEX_REASONING_ORDER = ["low", "medium", "high", "xhigh", "max", "ultra"];
|
|
6
|
-
|
|
7
|
-
function uniqueLevels(values) {
|
|
8
|
-
return [...new Set(values.filter(Boolean))];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
function normalizedEfforts(entry) {
|
|
13
|
-
if (!Array.isArray(entry)) return null;
|
|
14
|
-
const levels = entry.map(String).filter((value) => (
|
|
15
|
-
CODEX_REASONING_ORDER.includes(value) || value === "none" || value === "minimal"
|
|
16
|
-
));
|
|
17
|
-
return levels.length ? uniqueLevels(levels) : null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function resolveModelRecord(records, modelId) {
|
|
21
|
-
if (!records || typeof records !== "object") return undefined;
|
|
22
|
-
if (Object.prototype.hasOwnProperty.call(records, modelId)) return records[modelId];
|
|
23
|
-
const folded = modelId.toLowerCase();
|
|
24
|
-
for (const [key, value] of Object.entries(records)) {
|
|
25
|
-
if (key.toLowerCase() === folded) return value;
|
|
26
|
-
}
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function mergeCapability(providerConfig, modelId) {
|
|
31
|
-
if (!providerConfig) return null;
|
|
32
|
-
const configuredEfforts = normalizedEfforts(
|
|
33
|
-
resolveModelRecord(providerConfig.modelReasoningEfforts, modelId),
|
|
34
|
-
);
|
|
35
|
-
if (configuredEfforts) {
|
|
36
|
-
return {
|
|
37
|
-
source: "opencodex",
|
|
38
|
-
kind: "effort",
|
|
39
|
-
levels: uniqueLevels(configuredEfforts),
|
|
40
|
-
map: resolveModelRecord(providerConfig.modelReasoningEffortMap, modelId) ?? null,
|
|
41
|
-
rawEfforts: configuredEfforts,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
if (Array.isArray(providerConfig.noReasoningModels) && providerConfig.noReasoningModels.includes(modelId)) {
|
|
45
|
-
return { source: "opencodex", kind: "no-effort", levels: null, map: null, rawEfforts: [] };
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function loadModelCapabilities({
|
|
51
|
-
opencodexConfigPath = process.env.OPENCODEX_CONFIG_PATH ?? path.join(os.homedir(), ".opencodex", "config.json"),
|
|
52
|
-
read = readFile,
|
|
53
|
-
} = {}) {
|
|
54
|
-
const configPayload = await read(opencodexConfigPath, "utf8").then((value) => JSON.parse(value)).catch(() => null);
|
|
55
|
-
const providers = configPayload?.providers ?? {};
|
|
56
|
-
const result = new Map();
|
|
57
|
-
for (const [providerId, providerConfig] of Object.entries(providers)) {
|
|
58
|
-
const modelIds = new Set([
|
|
59
|
-
...Object.keys(providerConfig?.modelReasoningEfforts ?? {}),
|
|
60
|
-
...Object.keys(providerConfig?.modelReasoningEffortMap ?? {}),
|
|
61
|
-
...Object.keys(providerConfig?.thinkingToggleModels ?? {}),
|
|
62
|
-
...Object.keys(providerConfig?.thinkingBudgetModels ?? {}),
|
|
63
|
-
...(Array.isArray(providerConfig?.noReasoningModels) ? providerConfig.noReasoningModels : []),
|
|
64
|
-
]);
|
|
65
|
-
for (const modelId of modelIds) {
|
|
66
|
-
const capability = mergeCapability(providerConfig, modelId);
|
|
67
|
-
if (capability) result.set(`${providerId}/${modelId}`.toLowerCase(), capability);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return result;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function applyVisibleReasoningLevels(catalogModel, capability, fallbackLevels) {
|
|
74
|
-
if (!catalogModel) return catalogModel;
|
|
75
|
-
const rawLevels = Array.isArray(fallbackLevels) ? fallbackLevels : (catalogModel.supportedReasoningLevels ?? []);
|
|
76
|
-
if (!Array.isArray(rawLevels)) return catalogModel;
|
|
77
|
-
if (capability && Array.isArray(capability.levels)) {
|
|
78
|
-
const allowed = new Set(capability.levels);
|
|
79
|
-
const visible = rawLevels.filter((level) => {
|
|
80
|
-
const effort = String(level?.effort ?? level ?? "").toLowerCase();
|
|
81
|
-
return allowed.has(effort);
|
|
82
|
-
});
|
|
83
|
-
return { ...catalogModel, supportedReasoningLevels: visible, modelSelectorCapability: capability };
|
|
84
|
-
}
|
|
85
|
-
return { ...catalogModel, modelSelectorCapability: capability ?? null };
|
|
86
|
-
}
|