minecodex 0.2.1 → 0.2.3
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/README.md +2 -0
- package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +2 -1
- package/package.json +1 -1
- package/packages/cli/src/commands.mjs +149 -87
- package/packages/cli/src/progress.mjs +56 -0
- package/packages/runtime-host/src/codex-runtime.mjs +187 -17
- package/packages/runtime-host/src/feature-registry.mjs +6 -0
- package/packages/runtime-host/src/main.mjs +11 -1
- package/packages/runtime-host/src/model-capabilities.mjs +86 -0
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@ mcx install
|
|
|
26
26
|
|
|
27
27
|
运行 `mcx install` 不会强制关闭或启动 Codex,也不会影响当前打开的窗口。安装完成后,终端会说明更改将在下一次重启 Codex 时生效,并询问是否立即重启;默认选项为 **No**。只有在您明确确认,或后续手动运行 `mcx restart` 时,才会重新打开 Codex。安装完成后,macOS 的“登录项与扩展”中会添加名为 **MineCodex** 的后台项目。
|
|
28
28
|
|
|
29
|
+
`mcx install`、`mcx update`、`mcx restart` 和 `mcx uninstall` 会在终端持续显示当前阶段;`mcx open` 仅在需要启动后台服务时显示进度。查询类命令保持简洁,便于直接阅读或用于脚本。
|
|
30
|
+
|
|
29
31
|
MineCodex 只以后台辅助服务运行,前台应用始终是 OpenAI 签名的官方 `ChatGPT.app`,并继续使用官方默认用户数据。通过 Dock、Finder、Spotlight、登录项或会话恢复打开 ChatGPT 时,后台服务会验证官方应用与对应进程;若该进程尚未开放兼容的本机调试端口,则确认其完整退出后再通过 LaunchServices 重新打开官方应用并注入功能。MineCodex 不修改 `ChatGPT.app`、`app.asar`、Dock 项目或默认打开方式。
|
|
30
32
|
|
|
31
33
|
打开本地控制台:
|
|
@@ -15,7 +15,8 @@ function isUpsellBanner(element) {
|
|
|
15
15
|
if (!text) return false;
|
|
16
16
|
const isCard = element.classList.contains("rounded-2xl")
|
|
17
17
|
&& element.classList.contains("border")
|
|
18
|
-
&& element.classList.contains("bg-token-main-surface-primary")
|
|
18
|
+
&& (element.classList.contains("bg-token-main-surface-primary")
|
|
19
|
+
|| element.classList.contains("bg-surface"));
|
|
19
20
|
if (!isCard) return false;
|
|
20
21
|
const titleMatch = /out of Codex and Work usage/i.test(text)
|
|
21
22
|
|| /out of Codex messages/i.test(text)
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
4
4
|
import { assertNodeSupported, MacPlatformAdapter } from "./platform.mjs";
|
|
5
5
|
import { ensureConfig, ensureControlToken, readConfig } from "./config.mjs";
|
|
6
6
|
import { createControlServer } from "./control-server.mjs";
|
|
7
|
+
import { createProgressReporter } from "./progress.mjs";
|
|
7
8
|
import { createNpmAdapter } from "./npm-adapter.mjs";
|
|
8
9
|
import { resolvePaths } from "./paths.mjs";
|
|
9
10
|
import { RuntimeManager, writeServicePid } from "./runtime-manager.mjs";
|
|
@@ -118,20 +119,37 @@ export async function confirmRestartNow({ input = process.stdin, terminalOutput
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
async function installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion = process.versions.node }) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const previousService = await serviceSnapshot(platform, paths);
|
|
125
|
-
await ensureConfig(paths.configPath);
|
|
126
|
-
await ensureControlToken(paths.tokenPath);
|
|
122
|
+
const reporter = createProgressReporter({ output });
|
|
123
|
+
reporter.heading("Installing MineCodex");
|
|
124
|
+
let previousService = null;
|
|
127
125
|
try {
|
|
128
|
-
await
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
await reporter.step(1, 6, "Checking your system", async () => {
|
|
127
|
+
assertNodeSupported(nodeVersion);
|
|
128
|
+
platform.assertInstallSupported();
|
|
129
|
+
await platform.assertCodexInstalled();
|
|
130
|
+
});
|
|
131
|
+
await reporter.step(2, 6, "Preparing your settings", async () => {
|
|
132
|
+
await ensureConfig(paths.configPath);
|
|
133
|
+
await ensureControlToken(paths.tokenPath);
|
|
134
|
+
});
|
|
135
|
+
previousService = await reporter.step(3, 6, "Checking your current setup", async () => {
|
|
136
|
+
return serviceSnapshot(platform, paths);
|
|
137
|
+
});
|
|
138
|
+
await reporter.step(4, 6, "Installing the background helper", async () => {
|
|
139
|
+
await platform.installService({ cliPath, paths, start: true });
|
|
140
|
+
});
|
|
141
|
+
await reporter.step(5, 6, "Waiting for the background helper (up to 20 seconds)", async () => {
|
|
142
|
+
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
143
|
+
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))) {
|
|
144
|
+
throw new Error("MineCodex service did not become ready. Check " + paths.serviceErrorLogPath);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
await reporter.step(6, 6, "Waiting for MineCodex to be ready (up to 30 seconds)", async () => {
|
|
148
|
+
const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
|
|
149
|
+
if (!(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
|
|
150
|
+
throw new Error("MineCodex service did not become ready. Check " + paths.serviceErrorLogPath);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
135
153
|
} catch (error) {
|
|
136
154
|
let rollbackError = null;
|
|
137
155
|
try {
|
|
@@ -141,29 +159,38 @@ async function installCommand({ paths, platform, cliPath, output, serviceProbe,
|
|
|
141
159
|
}
|
|
142
160
|
throw appendRollbackError(error, rollbackError);
|
|
143
161
|
}
|
|
144
|
-
|
|
162
|
+
reporter.heading("MineCodex installed. It will be enabled the next time Codex restarts.");
|
|
145
163
|
if (await confirmRestart()) {
|
|
146
164
|
await restartCommand({ paths, output, fetchImpl });
|
|
147
165
|
} else {
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
reporter.heading("Run `mcx restart` when you are ready.");
|
|
167
|
+
}
|
|
150
168
|
}
|
|
151
169
|
|
|
152
|
-
async function openCommand({ paths, platform, serviceProbe, fetchImpl }) {
|
|
170
|
+
async function openCommand({ paths, platform, output, serviceProbe, fetchImpl }) {
|
|
153
171
|
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
172
|
+
let reporter = null;
|
|
154
173
|
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl, timeoutMs: 750 }))) {
|
|
155
174
|
if (!(await serviceInstalled(platform, paths))) throw new Error("MineCodex is not installed. Run `mcx install` first.");
|
|
156
|
-
|
|
157
|
-
|
|
175
|
+
reporter = createProgressReporter({ output });
|
|
176
|
+
reporter.heading("Opening MineCodex");
|
|
177
|
+
await reporter.step(1, 2, "Starting the background helper", async () => {
|
|
178
|
+
await platform.startService(paths);
|
|
179
|
+
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))) throw new Error("MineCodex service could not be started.");
|
|
180
|
+
});
|
|
158
181
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
182
|
+
const openControlPanel = async () => {
|
|
183
|
+
const exchangeResponse = await authorizedFetch(paths, "/api/auth/exchange", {
|
|
184
|
+
method: "POST",
|
|
185
|
+
body: "{}",
|
|
186
|
+
}, { fetchImpl });
|
|
187
|
+
if (!exchangeResponse.ok) throw new Error(`MineCodex browser exchange failed: HTTP ${exchangeResponse.status}`);
|
|
188
|
+
const { exchange } = await exchangeResponse.json();
|
|
189
|
+
if (!exchange) throw new Error("MineCodex browser exchange did not return a short-lived value.");
|
|
190
|
+
await platform.openBrowser(`${controlOrigin()}/auth?exchange=${encodeURIComponent(exchange)}`);
|
|
191
|
+
};
|
|
192
|
+
if (reporter) await reporter.step(2, 2, "Opening the control panel", openControlPanel);
|
|
193
|
+
else await openControlPanel();
|
|
167
194
|
}
|
|
168
195
|
|
|
169
196
|
function featureStatusText(feature) {
|
|
@@ -221,95 +248,130 @@ async function statusCommand({ paths, platform, jsonOutput, output, fetchImpl })
|
|
|
221
248
|
}
|
|
222
249
|
|
|
223
250
|
async function restartCommand({ paths, output, fetchImpl }) {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
251
|
+
const reporter = createProgressReporter({ output });
|
|
252
|
+
reporter.heading("Restarting MineCodex");
|
|
253
|
+
await reporter.step(1, 1, "Restarting Codex and applying plugin changes", async () => {
|
|
254
|
+
const response = await authorizedFetch(paths, "/api/restart", { method: "POST", body: "{}" }, { fetchImpl });
|
|
255
|
+
if (!response.ok) throw new Error(`MineCodex restart failed: HTTP ${response.status}`);
|
|
256
|
+
});
|
|
257
|
+
reporter.heading("MineCodex restarted and saved plugin switches are now applied.");
|
|
227
258
|
}
|
|
228
259
|
|
|
229
260
|
async function uninstallCommand({ paths, platform, purge, output, cliPath }) {
|
|
230
|
-
|
|
231
|
-
|
|
261
|
+
const reporter = createProgressReporter({ output });
|
|
262
|
+
reporter.heading("Uninstalling MineCodex");
|
|
263
|
+
const previousService = await reporter.step(1, 2, "Checking the current installation", async () => {
|
|
264
|
+
platform.assertInstallSupported();
|
|
265
|
+
return serviceSnapshot(platform, paths);
|
|
266
|
+
});
|
|
232
267
|
let wasManaged = false;
|
|
233
268
|
try {
|
|
234
|
-
await
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
269
|
+
await reporter.step(2, 2, purge ? "Removing MineCodex and its data" : "Removing the MineCodex integration", async () => {
|
|
270
|
+
await platform.removeService(paths);
|
|
271
|
+
wasManaged = await platform.terminateOwnedCodex(paths);
|
|
272
|
+
await platform.assertServiceStopped?.();
|
|
273
|
+
await platform.assertOwnedCodexStopped?.(paths);
|
|
274
|
+
if (purge) {
|
|
275
|
+
const targets = [paths.supportDir, paths.notesDataDir, paths.imagesDataDir];
|
|
276
|
+
for (const target of targets) {
|
|
277
|
+
if (!path.isAbsolute(target) || target === paths.homeDir || target.length <= paths.homeDir.length + 2) {
|
|
278
|
+
throw new Error(`Refusing unsafe purge target: ${target}`);
|
|
279
|
+
}
|
|
243
280
|
}
|
|
281
|
+
for (const target of targets) await rm(target, { recursive: true, force: true });
|
|
244
282
|
}
|
|
245
|
-
|
|
246
|
-
output("MineCodex integration and MineCodex-owned data were removed.");
|
|
247
|
-
} else {
|
|
248
|
-
output("MineCodex integration was removed. Plugin data was preserved.");
|
|
249
|
-
}
|
|
283
|
+
});
|
|
250
284
|
} catch (error) {
|
|
251
285
|
let rollbackError = null;
|
|
286
|
+
reporter.heading("Uninstall failed. Restoring the background helper.");
|
|
252
287
|
try {
|
|
253
|
-
await
|
|
288
|
+
await reporter.step(1, 1, "Restoring the background helper", async () => {
|
|
289
|
+
await restoreService(platform, previousService, { cliPath, paths });
|
|
290
|
+
});
|
|
254
291
|
} catch (failure) {
|
|
255
292
|
rollbackError = failure;
|
|
256
293
|
}
|
|
257
294
|
throw appendRollbackError(error, rollbackError);
|
|
258
295
|
}
|
|
259
|
-
|
|
260
|
-
|
|
296
|
+
reporter.heading(purge
|
|
297
|
+
? "MineCodex integration and MineCodex-owned data were removed."
|
|
298
|
+
: "MineCodex integration was removed. Plugin data was preserved.");
|
|
299
|
+
if (wasManaged) {
|
|
300
|
+
await reporter.step(1, 1, "Reopening Codex", async () => platform.openNativeCodex());
|
|
301
|
+
}
|
|
302
|
+
reporter.heading("Codex generated images and files referenced by Notes were not modified.");
|
|
261
303
|
}
|
|
262
304
|
|
|
263
305
|
async function updateCommand({ paths, platform, cliPath, output, npm = createNpmAdapter(), serviceProbe, fetchImpl }) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
306
|
+
const reporter = createProgressReporter({ output });
|
|
307
|
+
reporter.heading("Updating MineCodex");
|
|
308
|
+
const latest = await reporter.step(1, 5, "Checking for updates", async () => {
|
|
309
|
+
platform.assertInstallSupported();
|
|
310
|
+
const version = await npm.viewLatestVersion();
|
|
311
|
+
if (!version) throw new Error("Could not resolve the latest MineCodex version.");
|
|
312
|
+
return version;
|
|
313
|
+
});
|
|
267
314
|
if (latest === packageMetadata.version) {
|
|
268
|
-
|
|
315
|
+
reporter.heading(`MineCodex ${packageMetadata.version} is already up to date.`);
|
|
269
316
|
return;
|
|
270
317
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
318
|
+
let wasInstalled;
|
|
319
|
+
let previousService;
|
|
320
|
+
const rollback = await reporter.step(2, 5, "Backing up the current version", async () => {
|
|
321
|
+
wasInstalled = await serviceInstalled(platform, paths);
|
|
322
|
+
previousService = wasInstalled
|
|
323
|
+
? (await serviceSnapshot(platform, paths) ?? { running: true, plist: { exists: true } })
|
|
324
|
+
: null;
|
|
325
|
+
return npm.createRollbackArchive(paths.packageRoot);
|
|
326
|
+
});
|
|
276
327
|
const rollbackPath = typeof rollback === "string" ? rollback : rollback.archivePath;
|
|
277
328
|
if (!rollbackPath) throw new Error("Could not create a rollback archive for the installed MineCodex version.");
|
|
278
329
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
await npm.installGlobal("minecodex@latest");
|
|
284
|
-
if (wasInstalled) {
|
|
285
|
-
await platform.installService({ cliPath, paths, start: previousService.running });
|
|
286
|
-
if (previousService.running) {
|
|
287
|
-
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
288
|
-
const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
|
|
289
|
-
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
|
|
290
|
-
|| !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
|
|
291
|
-
throw new Error("MineCodex service did not become ready after update.");
|
|
292
|
-
}
|
|
293
|
-
} else {
|
|
330
|
+
await reporter.step(3, 5, "Pausing the background helper", async () => {
|
|
331
|
+
if (wasInstalled && previousService?.running) {
|
|
332
|
+
await platform.stopService(paths, { ignoreErrors: false });
|
|
294
333
|
await platform.assertServiceStopped?.();
|
|
295
334
|
}
|
|
296
|
-
}
|
|
297
|
-
|
|
335
|
+
});
|
|
336
|
+
await reporter.step(4, 5, `Installing MineCodex ${latest}`, async () => {
|
|
337
|
+
await npm.installGlobal("minecodex@latest");
|
|
338
|
+
});
|
|
339
|
+
await reporter.step(5, 5, "Restoring the background helper", async () => {
|
|
340
|
+
if (wasInstalled) {
|
|
341
|
+
await platform.installService({ cliPath, paths, start: previousService.running });
|
|
342
|
+
if (previousService.running) {
|
|
343
|
+
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
344
|
+
const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
|
|
345
|
+
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
|
|
346
|
+
|| !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
|
|
347
|
+
throw new Error("MineCodex service did not become ready after update.");
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
await platform.assertServiceStopped?.();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
reporter.heading(`MineCodex updated from ${packageMetadata.version} to ${latest}.`);
|
|
298
355
|
} catch (error) {
|
|
299
356
|
let rollbackError = null;
|
|
357
|
+
reporter.heading("Update failed. Restoring the previous version.");
|
|
300
358
|
try {
|
|
301
|
-
await
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
359
|
+
await reporter.step(1, 2, "Restoring the previous version", async () => {
|
|
360
|
+
await npm.installGlobal(rollbackPath);
|
|
361
|
+
});
|
|
362
|
+
await reporter.step(2, 2, "Restoring the background helper", async () => {
|
|
363
|
+
await restoreService(platform, previousService, { cliPath, paths });
|
|
364
|
+
if (wasInstalled && previousService?.running) {
|
|
365
|
+
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
366
|
+
const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
|
|
367
|
+
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
|
|
368
|
+
|| !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
|
|
369
|
+
throw new Error("Rolled-back MineCodex service did not become ready.");
|
|
370
|
+
}
|
|
371
|
+
} else if (wasInstalled && !previousService?.running) {
|
|
372
|
+
await platform.assertServiceStopped?.();
|
|
309
373
|
}
|
|
310
|
-
}
|
|
311
|
-
await platform.assertServiceStopped?.();
|
|
312
|
-
}
|
|
374
|
+
});
|
|
313
375
|
} catch (failure) {
|
|
314
376
|
rollbackError = failure;
|
|
315
377
|
}
|
|
@@ -379,7 +441,7 @@ export async function runCli(argv, {
|
|
|
379
441
|
if (["--help", "-h", "help"].includes(command)) return printHelp(output);
|
|
380
442
|
if (["--version", "-v"].includes(command)) return output(packageMetadata.version);
|
|
381
443
|
if (command === "install") return installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion });
|
|
382
|
-
if (command === "open" || command === "gui") return openCommand({ paths, platform, serviceProbe, fetchImpl });
|
|
444
|
+
if (command === "open" || command === "gui") return openCommand({ paths, platform, output, serviceProbe, fetchImpl });
|
|
383
445
|
if (command === "status") return statusCommand({ paths, platform, jsonOutput: args.includes("--json"), output, fetchImpl });
|
|
384
446
|
if (command === "restart") return restartCommand({ paths, output, fetchImpl });
|
|
385
447
|
if (command === "uninstall" || command === "remove") {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
2
|
+
|
|
3
|
+
function createProgressReporter({ output = console.log, terminalOutput = process.stdout } = {}) {
|
|
4
|
+
// 只有真实终端才显示 spinner;测试或脚本环境保持普通逐行文本。
|
|
5
|
+
const interactive = output === console.log && Boolean(terminalOutput?.isTTY);
|
|
6
|
+
let timer = null;
|
|
7
|
+
|
|
8
|
+
function stopSpinner() {
|
|
9
|
+
if (timer) clearInterval(timer);
|
|
10
|
+
timer = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function writeLine(line) {
|
|
14
|
+
if (!interactive) {
|
|
15
|
+
output(line);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
terminalOutput.write(`\r\x1b[2K${line}\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function startSpinner(line) {
|
|
22
|
+
if (!interactive) {
|
|
23
|
+
output(line);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let frame = 0;
|
|
27
|
+
terminalOutput.write(`\r\x1b[2K${SPINNER_FRAMES[frame]} ${line}`);
|
|
28
|
+
timer = setInterval(() => {
|
|
29
|
+
frame = (frame + 1) % SPINNER_FRAMES.length;
|
|
30
|
+
terminalOutput.write(`\r\x1b[2K${SPINNER_FRAMES[frame]} ${line}`);
|
|
31
|
+
}, 80);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
heading(line) {
|
|
36
|
+
writeLine(line);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async step(index, total, label, action) {
|
|
40
|
+
const prefix = `[${index}/${total}] ${label}`;
|
|
41
|
+
startSpinner(prefix);
|
|
42
|
+
try {
|
|
43
|
+
const result = await action();
|
|
44
|
+
stopSpinner();
|
|
45
|
+
writeLine(`✓ ${prefix}`);
|
|
46
|
+
return result;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
stopSpinner();
|
|
49
|
+
writeLine(`✗ ${prefix}`);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { createProgressReporter };
|
|
@@ -306,6 +306,7 @@ export function createInjectionSource(features, {
|
|
|
306
306
|
let modelSelectorSuppressOutsidePointerSequence = false;
|
|
307
307
|
let modelSelectorTriggerObserver = null;
|
|
308
308
|
let modelSelectorObservedTrigger = null;
|
|
309
|
+
let modelSelectorControllerMenu = null;
|
|
309
310
|
let modelSelectorNativeFastIcons = null;
|
|
310
311
|
// Fast 图标来源标记:bundle 静态提取 vs DOM 克隆,DOM 克隆优先以保持像素一致。
|
|
311
312
|
let modelSelectorNativeFastIconSources = null;
|
|
@@ -318,6 +319,7 @@ export function createInjectionSource(features, {
|
|
|
318
319
|
modelSelectorStyle.setAttribute("data-codex-model-slider-style", "");
|
|
319
320
|
modelSelectorStyle.textContent = `
|
|
320
321
|
[data-codex-model-slider-menu] { width: 264px !important; min-width: 264px; overflow-x: hidden; }
|
|
322
|
+
[data-codex-model-slider-controller-menu] { position: absolute !important; right: 0; bottom: 0; z-index: 1; }
|
|
321
323
|
[data-codex-model-slider-menu][data-codex-model-slider-overflow] {
|
|
322
324
|
max-height: var(--codex-model-slider-menu-max-height) !important;
|
|
323
325
|
overflow-y: auto;
|
|
@@ -952,10 +954,21 @@ export function createInjectionSource(features, {
|
|
|
952
954
|
.trim();
|
|
953
955
|
}
|
|
954
956
|
|
|
957
|
+
function currentReactFiber(element) {
|
|
958
|
+
const fiberKey = Object.getOwnPropertyNames(element ?? {}).find((key) => key.startsWith("__reactFiber$"));
|
|
959
|
+
const propsKey = Object.getOwnPropertyNames(element ?? {}).find((key) => key.startsWith("__reactProps$"));
|
|
960
|
+
let fiber = fiberKey ? element[fiberKey] : null;
|
|
961
|
+
if (
|
|
962
|
+
propsKey
|
|
963
|
+
&& fiber?.alternate?.memoizedProps === element[propsKey]
|
|
964
|
+
&& fiber.memoizedProps !== element[propsKey]
|
|
965
|
+
) return fiber.alternate;
|
|
966
|
+
return fiber;
|
|
967
|
+
}
|
|
968
|
+
|
|
955
969
|
function nativeComposerModelController() {
|
|
956
970
|
const trigger = document.querySelector("[data-codex-intelligence-trigger]");
|
|
957
|
-
|
|
958
|
-
let fiber = fiberKey ? trigger[fiberKey] : null;
|
|
971
|
+
let fiber = currentReactFiber(trigger);
|
|
959
972
|
for (let depth = 0; fiber && depth < 40; depth += 1, fiber = fiber.return) {
|
|
960
973
|
const props = fiber.memoizedProps;
|
|
961
974
|
if (Array.isArray(props?.models) && typeof props.onSelectReasoningEffort === "function") return props;
|
|
@@ -1093,6 +1106,19 @@ export function createInjectionSource(features, {
|
|
|
1093
1106
|
return formatIdentifier(identity.backend).toLowerCase() === nativeLabel ? native : thirdParty;
|
|
1094
1107
|
}
|
|
1095
1108
|
|
|
1109
|
+
function visibleReasoningLevelsFor(model) {
|
|
1110
|
+
if (!model) return [];
|
|
1111
|
+
const capability = model.modelSelectorCapability;
|
|
1112
|
+
if (capability && Array.isArray(capability.levels)) {
|
|
1113
|
+
const allowed = new Set(capability.levels);
|
|
1114
|
+
return (model.supportedReasoningLevels ?? []).filter((level) => {
|
|
1115
|
+
const effort = String(level?.effort ?? level ?? "").toLowerCase();
|
|
1116
|
+
return allowed.has(effort);
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
return model.supportedReasoningLevels ?? [];
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1096
1122
|
function loadModelFavorites(selector) {
|
|
1097
1123
|
try {
|
|
1098
1124
|
const value = JSON.parse(localStorage.getItem(selector.favoriteStorageKey) ?? "[]");
|
|
@@ -1467,8 +1493,12 @@ export function createInjectionSource(features, {
|
|
|
1467
1493
|
renderEnhancedModelMenu(menu, selector);
|
|
1468
1494
|
}
|
|
1469
1495
|
|
|
1470
|
-
function enhanceModelItem(item, menu, selector, index
|
|
1471
|
-
|
|
1496
|
+
function enhanceModelItem(item, menu, selector, index, {
|
|
1497
|
+
rawValue = null,
|
|
1498
|
+
selected = null,
|
|
1499
|
+
scheduleReturn = true,
|
|
1500
|
+
} = {}) {
|
|
1501
|
+
const observedRaw = rawValue ?? modelItemRawValue(item);
|
|
1472
1502
|
if (!observedRaw) return;
|
|
1473
1503
|
const observedIdentity = modelIdentity(observedRaw);
|
|
1474
1504
|
const nativeDescriptor = nativeModelDescriptorForItem(item, observedRaw);
|
|
@@ -1486,7 +1516,10 @@ export function createInjectionSource(features, {
|
|
|
1486
1516
|
const display = modelDisplayLabel(identity, selector);
|
|
1487
1517
|
const provider = providerLabel(identity.provider, selector);
|
|
1488
1518
|
const brand = brandForModel(identity, selector);
|
|
1489
|
-
item.toggleAttribute(
|
|
1519
|
+
item.toggleAttribute(
|
|
1520
|
+
"data-codex-model-slider-selected",
|
|
1521
|
+
selected ?? Boolean(item.querySelector("svg")),
|
|
1522
|
+
);
|
|
1490
1523
|
const favorites = loadModelFavorites(selector);
|
|
1491
1524
|
const starred = favorites.has(raw);
|
|
1492
1525
|
const row = document.createElement("span");
|
|
@@ -1515,7 +1548,7 @@ export function createInjectionSource(features, {
|
|
|
1515
1548
|
star.append(createModelSelectorIcon(starred ? selector.icons.unstar : selector.icons.star));
|
|
1516
1549
|
row.append(star);
|
|
1517
1550
|
item.replaceChildren(row);
|
|
1518
|
-
if (!item.hasAttribute("data-codex-model-slider-return-listener")) {
|
|
1551
|
+
if (scheduleReturn && !item.hasAttribute("data-codex-model-slider-return-listener")) {
|
|
1519
1552
|
item.setAttribute("data-codex-model-slider-return-listener", "");
|
|
1520
1553
|
item.addEventListener("click", (event) => {
|
|
1521
1554
|
if (!event.target.closest?.("[data-codex-model-slider-star]")) scheduleModelSliderReturn();
|
|
@@ -1652,6 +1685,106 @@ export function createInjectionSource(features, {
|
|
|
1652
1685
|
}
|
|
1653
1686
|
}
|
|
1654
1687
|
|
|
1688
|
+
function closeControllerModelMenu() {
|
|
1689
|
+
const record = modelSelectorControllerMenu;
|
|
1690
|
+
if (!record) return false;
|
|
1691
|
+
record.button.setAttribute("aria-expanded", "false");
|
|
1692
|
+
record.parentMenu.style.position = record.position;
|
|
1693
|
+
record.parentMenu.style.overflow = record.overflow;
|
|
1694
|
+
for (const item of modelMenuItems(record.menu)) modelSelectorOriginalItems.delete(item);
|
|
1695
|
+
record.menu.remove();
|
|
1696
|
+
modelSelectorControllerMenu = null;
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function controllerModelReasoningEffort(controller, model) {
|
|
1701
|
+
const current = String(controller?.reasoningEffort ?? "").toLowerCase();
|
|
1702
|
+
const supported = (model?.supportedReasoningEfforts ?? []).map((entry) => (
|
|
1703
|
+
String(entry?.reasoningEffort ?? entry ?? "").toLowerCase()
|
|
1704
|
+
)).filter(Boolean);
|
|
1705
|
+
return supported.includes(current)
|
|
1706
|
+
? current
|
|
1707
|
+
: String(model?.defaultReasoningEffort ?? supported[0] ?? "none").toLowerCase();
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function openControllerModelMenu(parentMenu, modelButton, selector) {
|
|
1711
|
+
if (modelSelectorControllerMenu) {
|
|
1712
|
+
closeControllerModelMenu();
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
const controller = nativeComposerModelController();
|
|
1716
|
+
if (!Array.isArray(controller?.models) || typeof controller.onSelectModel !== "function") return;
|
|
1717
|
+
const models = controller.models.filter((model) => !model.hidden && String(model.model ?? "").trim());
|
|
1718
|
+
if (!models.length) return;
|
|
1719
|
+
|
|
1720
|
+
const menu = document.createElement("div");
|
|
1721
|
+
menu.className = parentMenu.className;
|
|
1722
|
+
menu.setAttribute("role", "menu");
|
|
1723
|
+
menu.setAttribute("data-state", "open");
|
|
1724
|
+
menu.setAttribute("data-codex-model-slider-menu", "");
|
|
1725
|
+
menu.setAttribute("data-codex-model-slider-controller-menu", "");
|
|
1726
|
+
models.forEach((model, index) => {
|
|
1727
|
+
const item = document.createElement("div");
|
|
1728
|
+
item.className = "no-drag outline-hidden rounded-lg px-[var(--padding-row-x)] py-[var(--padding-row-y)] text-sm text-default group hover:bg-primary-ghost-hover focus:bg-primary-ghost-hover cursor-interaction flex flex-col";
|
|
1729
|
+
item.setAttribute("role", "menuitem");
|
|
1730
|
+
item.setAttribute("tabindex", "-1");
|
|
1731
|
+
enhanceModelItem(item, menu, selector, index, {
|
|
1732
|
+
rawValue: String(model.model),
|
|
1733
|
+
selected: String(model.model) === String(controller.model),
|
|
1734
|
+
scheduleReturn: false,
|
|
1735
|
+
});
|
|
1736
|
+
menu.append(item);
|
|
1737
|
+
});
|
|
1738
|
+
menu.addEventListener("click", (event) => {
|
|
1739
|
+
const star = event.target.closest?.("[data-codex-model-slider-star]");
|
|
1740
|
+
const item = event.target.closest?.("[data-codex-model-slider-item]");
|
|
1741
|
+
if (!item) return;
|
|
1742
|
+
if (star) {
|
|
1743
|
+
const raw = item.dataset.codexModelSliderRaw;
|
|
1744
|
+
const label = item.querySelector("[data-codex-model-slider-name]")?.textContent ?? raw;
|
|
1745
|
+
toggleModelFavorite(event, menu, selector, raw, label);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const currentController = nativeComposerModelController();
|
|
1749
|
+
const raw = item.dataset.codexModelSliderRaw;
|
|
1750
|
+
const model = currentController?.models?.find((candidate) => String(candidate.model) === raw);
|
|
1751
|
+
if (!model || typeof currentController.onSelectModel !== "function") return;
|
|
1752
|
+
const reasoningEffort = controllerModelReasoningEffort(currentController, model);
|
|
1753
|
+
currentController.onSelectModel(raw, reasoningEffort);
|
|
1754
|
+
closeControllerModelMenu();
|
|
1755
|
+
queueEnsure();
|
|
1756
|
+
});
|
|
1757
|
+
menu.addEventListener("keydown", (event) => {
|
|
1758
|
+
if (event.key === "Escape") {
|
|
1759
|
+
event.preventDefault();
|
|
1760
|
+
closeControllerModelMenu();
|
|
1761
|
+
modelButton.focus();
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1765
|
+
const item = event.target.closest?.("[data-codex-model-slider-item]");
|
|
1766
|
+
if (!item) return;
|
|
1767
|
+
event.preventDefault();
|
|
1768
|
+
item.click();
|
|
1769
|
+
});
|
|
1770
|
+
renderEnhancedModelMenu(menu, selector);
|
|
1771
|
+
|
|
1772
|
+
modelSelectorControllerMenu = {
|
|
1773
|
+
menu,
|
|
1774
|
+
parentMenu,
|
|
1775
|
+
button: modelButton,
|
|
1776
|
+
position: parentMenu.style.position,
|
|
1777
|
+
overflow: parentMenu.style.overflow,
|
|
1778
|
+
};
|
|
1779
|
+
parentMenu.style.position = "relative";
|
|
1780
|
+
parentMenu.style.overflow = "visible";
|
|
1781
|
+
parentMenu.append(menu);
|
|
1782
|
+
sizeModelMenuViewport(menu, selector.maxVisibleItems);
|
|
1783
|
+
modelButton.setAttribute("aria-expanded", "true");
|
|
1784
|
+
(menu.querySelector('[data-codex-model-slider-selected]')
|
|
1785
|
+
?? menu.querySelector('[role="menuitem"]'))?.focus();
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1655
1788
|
function openNativeModelSubmenu(parentMenu) {
|
|
1656
1789
|
let attempts = 0;
|
|
1657
1790
|
const open = () => {
|
|
@@ -1929,6 +2062,8 @@ export function createInjectionSource(features, {
|
|
|
1929
2062
|
modelButton.setAttribute("tabindex", "0");
|
|
1930
2063
|
modelButton.setAttribute("data-codex-model-slider-model-button", "");
|
|
1931
2064
|
modelButton.setAttribute("aria-label", `Model ${modelDisplayLabel(identity, selector)}`);
|
|
2065
|
+
modelButton.setAttribute("aria-haspopup", "menu");
|
|
2066
|
+
modelButton.setAttribute("aria-expanded", "false");
|
|
1932
2067
|
const buttonContent = document.createElement("span");
|
|
1933
2068
|
buttonContent.className = classes.ViewToggleContent;
|
|
1934
2069
|
const buttonLabel = document.createElement("span");
|
|
@@ -1942,7 +2077,7 @@ export function createInjectionSource(features, {
|
|
|
1942
2077
|
buttonContent.append(buttonLabel);
|
|
1943
2078
|
}
|
|
1944
2079
|
modelButton.append(buttonContent);
|
|
1945
|
-
const openModels = () =>
|
|
2080
|
+
const openModels = () => openControllerModelMenu(parentMenu, modelButton, selector);
|
|
1946
2081
|
modelButton.addEventListener("click", openModels);
|
|
1947
2082
|
modelButton.addEventListener("keydown", (event) => {
|
|
1948
2083
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
@@ -1993,6 +2128,11 @@ export function createInjectionSource(features, {
|
|
|
1993
2128
|
viewControls.append(modelButton);
|
|
1994
2129
|
}
|
|
1995
2130
|
|
|
2131
|
+
if (efforts.length < 2) {
|
|
2132
|
+
shell.append(viewControls);
|
|
2133
|
+
return shell;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
1996
2136
|
const nativeSlider = cloneNativePowerSlider(parentMenu, efforts.length, fastEnabled);
|
|
1997
2137
|
const rail = nativeSlider?.querySelector('[role="slider"]') ?? document.createElement("div");
|
|
1998
2138
|
rail.setAttribute("data-codex-model-slider-rail", "");
|
|
@@ -2106,8 +2246,14 @@ export function createInjectionSource(features, {
|
|
|
2106
2246
|
function enhanceGenericModelPicker(parentMenu, selector) {
|
|
2107
2247
|
const identity = modelIdentity(nativeModelValue(parentMenu));
|
|
2108
2248
|
const model = modelDefinitionFor(identity, selector);
|
|
2109
|
-
const
|
|
2110
|
-
|
|
2249
|
+
const catalogModel = catalogModelFor(identity, selector);
|
|
2250
|
+
const modelWithCapability = model?.modelSelectorCapability ? model : {
|
|
2251
|
+
...(model ?? {}),
|
|
2252
|
+
...(catalogModel ?? {}),
|
|
2253
|
+
supportedReasoningLevels: model?.supportedReasoningLevels ?? catalogModel?.supportedReasoningLevels ?? [],
|
|
2254
|
+
};
|
|
2255
|
+
const efforts = visibleReasoningLevelsFor(modelWithCapability);
|
|
2256
|
+
if (!model) return false;
|
|
2111
2257
|
// 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
|
|
2112
2258
|
// 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
|
|
2113
2259
|
const nativeModel = nativeCatalogModelFor(identity);
|
|
@@ -2115,10 +2261,22 @@ export function createInjectionSource(features, {
|
|
|
2115
2261
|
?? document.querySelector("[data-codex-intelligence-trigger]")
|
|
2116
2262
|
?.getAttribute("data-selected-reasoning-effort")
|
|
2117
2263
|
?? model.defaultReasoningLevel
|
|
2118
|
-
?? efforts[0]
|
|
2264
|
+
?? efforts[0]?.effort
|
|
2265
|
+
?? "none";
|
|
2266
|
+
const effortRank = (value) => {
|
|
2267
|
+
const order = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
2268
|
+
const index = order.indexOf(String(value ?? "").toLowerCase());
|
|
2269
|
+
return index < 0 ? -1 : index;
|
|
2270
|
+
};
|
|
2271
|
+
const visibleEffort = efforts.find((level) => level.effort === selectedEffort)?.effort
|
|
2272
|
+
?? efforts.filter((level) => effortRank(level.effort) <= effortRank(selectedEffort))
|
|
2273
|
+
.sort((left, right) => effortRank(right.effort) - effortRank(left.effort))[0]?.effort
|
|
2274
|
+
?? efforts[0]?.effort
|
|
2275
|
+
?? "none";
|
|
2276
|
+
const selectedEffortValue = visibleEffort;
|
|
2119
2277
|
const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
|
|
2120
2278
|
const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
|
|
2121
|
-
const key = `${identity.raw}:${
|
|
2279
|
+
const key = `${identity.raw}:${selectedEffortValue}:${serviceTierKey}:${efforts.map((level) => level.effort).join(",")}`;
|
|
2122
2280
|
const existing = parentMenu.querySelector(":scope > [data-codex-model-slider-generic]");
|
|
2123
2281
|
const nativeTemplateReady = Boolean(
|
|
2124
2282
|
parentMenu.firstElementChild
|
|
@@ -2140,7 +2298,7 @@ export function createInjectionSource(features, {
|
|
|
2140
2298
|
existing?.remove();
|
|
2141
2299
|
const nativeContainer = parentMenu.firstElementChild;
|
|
2142
2300
|
if (!nativeContainer) return false;
|
|
2143
|
-
const shell = createGenericModelSlider(parentMenu, selector, identity, model,
|
|
2301
|
+
const shell = createGenericModelSlider(parentMenu, selector, identity, model, selectedEffortValue);
|
|
2144
2302
|
if (!shell) return false;
|
|
2145
2303
|
if (!modelSelectorGenericContainers.has(nativeContainer)) {
|
|
2146
2304
|
modelSelectorGenericContainers.set(nativeContainer, nativeContainer.style.display);
|
|
@@ -2223,12 +2381,18 @@ export function createInjectionSource(features, {
|
|
|
2223
2381
|
if (!modelSelectorStyle.isConnected) document.head?.append(modelSelectorStyle);
|
|
2224
2382
|
enhanceComposerModelTrigger(selector);
|
|
2225
2383
|
const parentMenu = nativeModelPickerMenu();
|
|
2384
|
+
if (modelSelectorControllerMenu && modelSelectorControllerMenu.parentMenu !== parentMenu) {
|
|
2385
|
+
closeControllerModelMenu();
|
|
2386
|
+
}
|
|
2226
2387
|
if (!parentMenu) {
|
|
2227
|
-
if (
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2388
|
+
if (
|
|
2389
|
+
modelSelectorPendingReturn
|
|
2390
|
+
&& !visibleBlockingDialog()
|
|
2391
|
+
&& nativeModelPickerSubmenus().length === 0
|
|
2392
|
+
) {
|
|
2393
|
+
clearTimeout(modelSelectorReturnTimer);
|
|
2394
|
+
modelSelectorReturnTimer = null;
|
|
2395
|
+
returnToNativeModelSlider();
|
|
2232
2396
|
}
|
|
2233
2397
|
} else {
|
|
2234
2398
|
enhanceNativeModelPicker(parentMenu, selector);
|
|
@@ -2244,6 +2408,7 @@ export function createInjectionSource(features, {
|
|
|
2244
2408
|
}
|
|
2245
2409
|
|
|
2246
2410
|
function restoreModelSelectorEnhancements() {
|
|
2411
|
+
closeControllerModelMenu();
|
|
2247
2412
|
clearTimeout(modelSelectorReturnTimer);
|
|
2248
2413
|
clearTimeout(modelSelectorEffortTransactionTimer);
|
|
2249
2414
|
modelSelectorTriggerObserver?.disconnect();
|
|
@@ -3834,6 +3999,11 @@ export function createInjectionSource(features, {
|
|
|
3834
3999
|
document.addEventListener(
|
|
3835
4000
|
"click",
|
|
3836
4001
|
(event) => {
|
|
4002
|
+
if (
|
|
4003
|
+
modelSelectorControllerMenu
|
|
4004
|
+
&& !modelSelectorControllerMenu.menu.contains(event.target)
|
|
4005
|
+
&& !modelSelectorControllerMenu.button.contains(event.target)
|
|
4006
|
+
) closeControllerModelMenu();
|
|
3837
4007
|
if (isNativeSummaryButton(event.target) && activePinnedFeatureId) hidePinnedSurfaces();
|
|
3838
4008
|
if (activePinnedFeatureId && summaryDisplayMode() === "overlay") {
|
|
3839
4009
|
const state = summaryState(activePinnedFeatureId);
|
|
@@ -359,6 +359,12 @@ export async function loadConfiguredModelCatalog(
|
|
|
359
359
|
id: String(tier.id ?? "").trim(),
|
|
360
360
|
name: String(tier.name ?? "").trim(),
|
|
361
361
|
})),
|
|
362
|
+
capabilityKey: [
|
|
363
|
+
model.opencodex_capability_provenance?.provider,
|
|
364
|
+
model.opencodex_capability_provenance?.model_id,
|
|
365
|
+
].every((value) => typeof value === "string" && value.trim())
|
|
366
|
+
? `${model.opencodex_capability_provenance.provider.trim()}/${model.opencodex_capability_provenance.model_id.trim()}`
|
|
367
|
+
: null,
|
|
362
368
|
})).filter((model) => model.slug);
|
|
363
369
|
} catch (error) {
|
|
364
370
|
if (error.code === "ENOENT") return [];
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
selectEnabledFeatures,
|
|
12
12
|
startFeatureProcesses,
|
|
13
13
|
} from "./feature-registry.mjs";
|
|
14
|
+
import { applyVisibleReasoningLevels, loadModelCapabilities } from "./model-capabilities.mjs";
|
|
14
15
|
|
|
15
16
|
const runtimeRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
16
17
|
const featuresRoot = process.env.CODEX_FEATURES_ROOT ?? path.dirname(runtimeRoot);
|
|
@@ -22,8 +23,17 @@ const discoveredFeatures = await discoverFeatures(featuresRoot);
|
|
|
22
23
|
if (!discoveredFeatures.length) throw new Error(`No codex-feature.json files found under ${featuresRoot}`);
|
|
23
24
|
const features = selectEnabledFeatures(discoveredFeatures, process.env.MINECODEX_ENABLED_FEATURES);
|
|
24
25
|
const modelCatalog = await loadConfiguredModelCatalog();
|
|
26
|
+
const modelCapabilities = await loadModelCapabilities();
|
|
25
27
|
for (const feature of features) {
|
|
26
|
-
if (feature.modelSelector)
|
|
28
|
+
if (feature.modelSelector) {
|
|
29
|
+
feature.modelSelector.models = modelCatalog.map((model) => (
|
|
30
|
+
applyVisibleReasoningLevels(
|
|
31
|
+
model,
|
|
32
|
+
modelCapabilities.get(String(model.capabilityKey ?? model.slug ?? "").toLowerCase()),
|
|
33
|
+
model.supportedReasoningLevels,
|
|
34
|
+
)
|
|
35
|
+
));
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
let readyWritePromise = Promise.resolve();
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
}
|