diff --git a/cli.js b/cli.js index 039e7a1..c3601a7 100644 --- a/cli.js +++ b/cli.js @@ -223,6 +223,239 @@ function resolveWebPort() { return parsed; } +// #region releaseRunPortIfNeeded +function releaseRunPortIfNeeded(port, host, deps = {}) { + const numericPort = parseInt(String(port), 10); + if (numericPort !== DEFAULT_WEB_PORT) { + return { attempted: false, released: false, pids: [], reason: 'non-default-port' }; + } + + const processRef = deps.process || process; + const runSpawnSync = deps.spawnSync || spawnSync; + const logger = deps.logger || console; + const killProcess = typeof deps.kill === 'function' + ? deps.kill + : (typeof processRef.kill === 'function' ? processRef.kill.bind(processRef) : null); + const seenPids = new Set(); + const candidatePids = new Set(); + const currentPid = Number(processRef.pid); + const normalizedHost = typeof host === 'string' ? host.trim().toLowerCase() : ''; + let released = false; + const windowsCommandLineCache = new Map(); + + const isManagedRunCommand = (commandLine) => { + const normalizedLine = ` ${String(commandLine || '').replace(/\s+/g, ' ').trim()} `; + return /(^|[\/\\\s])codexmate(?:\.cmd|\.exe)? run(\s|$)/i.test(normalizedLine) + || /(^|[\/\\\s])cli\.js run(\s|$)/i.test(normalizedLine); + }; + + const normalizeListenerHost = (value) => { + const trimmed = String(value || '').trim().toLowerCase(); + if (!trimmed) { + return ''; + } + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + return trimmed.slice(1, -1); + } + return trimmed.startsWith('::ffff:') ? trimmed.slice('::ffff:'.length) : trimmed; + }; + + const extractListenerHost = (localAddress) => { + const trimmed = String(localAddress || '').trim(); + if (!trimmed) { + return ''; + } + if (trimmed.startsWith('[')) { + const closingBracket = trimmed.indexOf(']'); + if (closingBracket > 0) { + return normalizeListenerHost(trimmed.slice(1, closingBracket)); + } + } + const lastColon = trimmed.lastIndexOf(':'); + if (lastColon <= 0) { + return normalizeListenerHost(trimmed); + } + return normalizeListenerHost(trimmed.slice(0, lastColon)); + }; + + const isMatchingWindowsListenerAddress = (localAddress) => { + const listenerHost = extractListenerHost(localAddress); + if (!listenerHost || !normalizedHost) { + return false; + } + if (normalizedHost === 'localhost') { + return listenerHost === '127.0.0.1' || listenerHost === '::1'; + } + if (normalizedHost === '0.0.0.0' || normalizedHost === '::') { + return listenerHost === normalizedHost; + } + return listenerHost === normalizeListenerHost(normalizedHost); + }; + + const addPidsFromText = (text, targetSet = seenPids) => { + if (!targetSet) { + return; + } + const lines = String(text || '').split(/\r?\n/); + for (const line of lines) { + const tokens = line.trim().split(/\s+/).filter(Boolean); + for (const token of tokens) { + if (!/^\d+$/.test(token)) { + continue; + } + targetSet.add(Number(token)); + } + } + }; + + const runCommand = (command, args, options = {}) => { + const { + stdoutPidSet = seenPids, + stderrPidSet = seenPids + } = options; + const result = runSpawnSync(command, args, { encoding: 'utf8' }); + if (result && result.stdout) addPidsFromText(result.stdout, stdoutPidSet); + if (result && result.stderr) addPidsFromText(result.stderr, stderrPidSet); + return result || {}; + }; + + const addManagedRunPidsFromPs = (text, allowedPids = null) => { + const lines = String(text || '').split(/\r?\n/); + for (const line of lines) { + const normalizedLine = ` ${line.replace(/\s+/g, ' ').trim()} `; + if (!/(^|[\/\s])codexmate run(\s|$)/.test(normalizedLine) && !/(^|[\/\s])cli\.js run(\s|$)/.test(normalizedLine)) { + continue; + } + const pidMatch = line.match(/^\S+\s+(\d+)\s+/); + if (!pidMatch) { + continue; + } + const pid = Number(pidMatch[1]); + if (!Number.isFinite(pid) || pid <= 0 || pid === currentPid) { + continue; + } + if (allowedPids && !allowedPids.has(pid)) { + continue; + } + seenPids.add(pid); + } + }; + + const getWindowsProcessCommandLine = (pid) => { + if (windowsCommandLineCache.has(pid)) { + return windowsCommandLineCache.get(pid); + } + const result = runCommand( + 'powershell', + [ + '-NoProfile', + '-Command', + `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($p) { $p.CommandLine }` + ], + { stdoutPidSet: null, stderrPidSet: null } + ); + const commandLine = !result.error && result.status === 0 + ? String(result.stdout || '').trim() + : ''; + windowsCommandLineCache.set(pid, commandLine); + return commandLine; + }; + + if (processRef.platform === 'win32') { + const netstatResult = runCommand('netstat', ['-ano', '-p', 'tcp'], { stdoutPidSet: null, stderrPidSet: null }); + if (!(netstatResult && netstatResult.error)) { + const lines = String(netstatResult.stdout || '').split(/\r?\n/); + for (const line of lines) { + const parts = line.trim().split(/\s+/); + if (parts.length < 5) { + continue; + } + const localAddress = parts[1]; + const state = parts[3]; + const pidText = parts[4]; + if (state !== 'LISTENING' || !localAddress.endsWith(`:${numericPort}`) || !/^\d+$/.test(pidText)) { + continue; + } + if (!isMatchingWindowsListenerAddress(localAddress)) { + continue; + } + candidatePids.add(Number(pidText)); + } + for (const pid of candidatePids) { + if (pid === currentPid) { + continue; + } + if (!isManagedRunCommand(getWindowsProcessCommandLine(pid))) { + continue; + } + seenPids.add(pid); + const taskkillResult = runCommand( + 'taskkill', + ['/PID', String(pid), '/F'], + { stdoutPidSet: null, stderrPidSet: null } + ); + if (!taskkillResult.error && taskkillResult.status === 0) { + released = true; + } + } + } + } else { + let psResult = null; + const readPsResult = () => { + if (psResult) { + return psResult; + } + psResult = runCommand('ps', ['-ef'], { stdoutPidSet: null, stderrPidSet: null }); + return psResult; + }; + + const lsofResult = runCommand( + 'lsof', + ['-ti', `tcp:${numericPort}`], + { stdoutPidSet: candidatePids, stderrPidSet: null } + ); + const shouldTryFuser = !!(lsofResult && lsofResult.error && lsofResult.error.code === 'ENOENT'); + if (shouldTryFuser && candidatePids.size === 0) { + runCommand( + 'fuser', + [`${numericPort}/tcp`], + { stdoutPidSet: candidatePids, stderrPidSet: candidatePids } + ); + } + if (candidatePids.size > 0) { + const managedPsResult = readPsResult(); + if (!(managedPsResult && managedPsResult.error)) { + addManagedRunPidsFromPs(managedPsResult.stdout, candidatePids); + } + } + } + + if (processRef.platform !== 'win32' && killProcess && !released && seenPids.size > 0) { + for (const pid of seenPids) { + if (pid === currentPid) { + continue; + } + try { + killProcess(pid, 'SIGKILL'); + released = true; + } catch (_) {} + } + } + + if (released) { + logger.log(`~ 已释放端口 ${numericPort} 占用`); + } + + return { + attempted: true, + released, + pids: Array.from(seenPids) + .filter((pid) => pid !== currentPid) + .sort((a, b) => a - b) + }; +} +// #endregion releaseRunPortIfNeeded + function resolveWebHost(options = {}) { const optionHost = typeof options.host === 'string' ? options.host.trim() : ''; if (optionHost) { @@ -236,7 +469,6 @@ function resolveWebHost(options = {}) { } const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex" -model_reasoning_effort = "high" model_context_window = ${DEFAULT_MODEL_CONTEXT_WINDOW} model_auto_compact_token_limit = ${DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT} disable_response_storage = true @@ -10845,6 +11077,7 @@ function cmdStart(options = {}) { const port = resolveWebPort(); const host = resolveWebHost(options); + releaseRunPortIfNeeded(port, host); let serverHandle = createWebServer({ htmlPath, diff --git a/tests/e2e/test-config.js b/tests/e2e/test-config.js index 7d399d9..9cedd54 100644 --- a/tests/e2e/test-config.js +++ b/tests/e2e/test-config.js @@ -408,6 +408,10 @@ preferred_auth_method = "shadow-key" /^\s*model_auto_compact_token_limit\s*=\s*185000\s*$/m.test(legacyTemplateDefaults.template), 'legacy get-config-template should restore default model_auto_compact_token_limit' ); + assert( + !/^\s*model_reasoning_effort\s*=.+$/m.test(legacyTemplateDefaults.template), + 'legacy get-config-template should keep default medium reasoning without model_reasoning_effort' + ); const legacyAddDup = await legacyApi('add-provider', { name: 'foo.bar', url: 'https://dup.example.com/v1', diff --git a/tests/unit/provider-share-command.test.mjs b/tests/unit/provider-share-command.test.mjs index c9a1e89..94f441d 100644 --- a/tests/unit/provider-share-command.test.mjs +++ b/tests/unit/provider-share-command.test.mjs @@ -757,6 +757,87 @@ test('loadAll can refresh in background without flipping the global loading stat assert.strictEqual(context.initError, ''); }); +test('loadAll falls back to medium for unsupported reasoning effort values while preserving xhigh', async () => { + const loadAllSource = extractBlockBySignature( + appSource, + 'async loadAll(options = {}) {' + ).replace(/^async loadAll/, 'async function loadAll'); + const responses = [ + { + provider: 'alpha', + model: 'alpha-model', + serviceTier: 'fast', + modelReasoningEffort: 'bogus', + modelContextWindow: 200000, + modelAutoCompactTokenLimit: 180000, + configReady: true, + initNotice: '' + }, + { + provider: 'alpha', + model: 'alpha-model', + serviceTier: 'fast', + modelReasoningEffort: 'xhigh', + modelContextWindow: 200000, + modelAutoCompactTokenLimit: 180000, + configReady: true, + initNotice: '' + } + ]; + let statusIndex = 0; + const loadAll = instantiateFunction(loadAllSource, 'loadAll', { + defaultModelContextWindow: 190000, + defaultModelAutoCompactTokenLimit: 185000, + api: async (action) => { + if (action === 'status') { + return responses[statusIndex++] || responses[responses.length - 1]; + } + if (action === 'list') { + return { + providers: [{ name: 'alpha', url: 'https://api.example.com/v1', hasKey: true }] + }; + } + throw new Error(`Unexpected api action: ${action}`); + } + }); + + const createContext = () => ({ + loading: false, + initError: '', + currentProvider: 'stale-provider', + currentModel: 'stale-model', + serviceTier: 'fast', + modelReasoningEffort: 'high', + modelContextWindowInput: '190000', + modelAutoCompactTokenLimitInput: '185000', + editingCodexBudgetField: '', + providersList: [], + normalizePositiveIntegerInput(value, label, fallback = '') { + const raw = value === undefined || value === null || value === '' + ? String(fallback || '') + : String(value); + const text = raw.trim(); + const numeric = Number.parseInt(text, 10); + if (!Number.isFinite(numeric) || numeric <= 0) { + return { ok: false, error: `${label} invalid` }; + } + return { ok: true, value: numeric, text: String(numeric) }; + }, + showMessage() {}, + maybeShowStarPrompt() {}, + async loadModelsForProvider() {}, + async loadCodexAuthProfiles() {} + }); + + const invalidContext = createContext(); + await loadAll.call(invalidContext); + assert.strictEqual(invalidContext.modelReasoningEffort, 'medium'); + + const xhighContext = createContext(); + await loadAll.call(xhighContext); + assert.strictEqual(xhighContext.modelReasoningEffort, 'xhigh'); +}); + test('loadAll treats provider list fetch failures as startup errors and skips model refresh', async () => { const loadAllSource = extractBlockBySignature( appSource, diff --git a/tests/unit/web-run-host.test.mjs b/tests/unit/web-run-host.test.mjs index 892f87a..d3e4d55 100644 --- a/tests/unit/web-run-host.test.mjs +++ b/tests/unit/web-run-host.test.mjs @@ -118,6 +118,16 @@ const resolveWebHostSource = extractFunctionBySignature( 'function resolveWebHost(options = {}) {', 'resolveWebHost' ); +const cmdStartSource = extractFunctionBySignature( + cliContent, + 'function cmdStart(options = {}) {', + 'cmdStart' +); +const releaseRunPortIfNeededSource = extractFunctionBySignature( + cliContent, + 'function releaseRunPortIfNeeded(port, host, deps = {}) {', + 'releaseRunPortIfNeeded' +); const resolveWebHost = instantiateFunction(resolveWebHostSource, 'resolveWebHost', { DEFAULT_WEB_HOST: defaultHostMatch[1], process: { env: {} } @@ -153,6 +163,277 @@ test('web auto-open uses IPv6 loopback when binding to IPv6 any address', () => ); }); +test('releaseRunPortIfNeeded skips non-default ports', () => { + const calls = []; + const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', { + DEFAULT_WEB_PORT: 3737, + spawnSync(command, args) { + calls.push([command, args]); + return { status: 0, stdout: '', stderr: '' }; + }, + process: { platform: 'linux' }, + console: { log() {} } + }); + + const result = releaseRunPortIfNeeded(3999, '0.0.0.0'); + assert.deepStrictEqual(result, { + attempted: false, + released: false, + pids: [], + reason: 'non-default-port' + }); + assert.deepStrictEqual(calls, []); +}); + +test('releaseRunPortIfNeeded clears default port only after lsof pids map to managed run processes', () => { + const calls = []; + const killed = []; + const logs = []; + const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', { + DEFAULT_WEB_PORT: 3737, + spawnSync(command, args) { + calls.push([command, args]); + if (command === 'lsof') { + return { status: 0, stdout: '1234\n8888\n', stderr: '' }; + } + if (command === 'ps') { + return { + status: 0, + stdout: [ + 'UID PID PPID C STIME TTY TIME CMD', + 'u0_a876 1234 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser', + 'u0_a876 8888 1000 0 1970 ? 00:00:00 node /repo/other-server.js' + ].join('\n'), + stderr: '' + }; + } + throw new Error(`unexpected command: ${command}`); + }, + process: { + platform: 'linux', + kill(pid, signal) { + killed.push([pid, signal]); + } + }, + console: { log(message) { logs.push(message); } } + }); + + const result = releaseRunPortIfNeeded(3737, '0.0.0.0'); + assert.deepStrictEqual(calls, [ + ['lsof', ['-ti', 'tcp:3737']], + ['ps', ['-ef']] + ]); + assert.deepStrictEqual(killed, [[1234, 'SIGKILL']]); + assert.deepStrictEqual(result, { + attempted: true, + released: true, + pids: [1234] + }); + assert.deepStrictEqual(logs, ['~ 已释放端口 3737 占用']); +}); + +test('releaseRunPortIfNeeded falls back to non-destructive fuser pids when lsof is unavailable', () => { + const calls = []; + const killed = []; + const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', { + DEFAULT_WEB_PORT: 3737, + spawnSync(command, args) { + calls.push([command, args]); + if (command === 'lsof') { + return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' }; + } + if (command === 'fuser') { + return { status: 0, stdout: '', stderr: '2222 3333' }; + } + if (command === 'ps') { + return { + status: 0, + stdout: [ + 'UID PID PPID C STIME TTY TIME CMD', + 'u0_a876 2222 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser', + 'u0_a876 3333 1000 0 1970 ? 00:00:00 /usr/bin/codexmate run' + ].join('\n'), + stderr: '' + }; + } + throw new Error(`unexpected command: ${command}`); + }, + process: { + platform: 'linux', + kill(pid, signal) { + killed.push([pid, signal]); + } + }, + console: { log() {} } + }); + + const result = releaseRunPortIfNeeded(3737, '0.0.0.0'); + assert.deepStrictEqual(calls, [ + ['lsof', ['-ti', 'tcp:3737']], + ['fuser', ['3737/tcp']], + ['ps', ['-ef']] + ]); + assert.deepStrictEqual(killed, [ + [2222, 'SIGKILL'], + [3333, 'SIGKILL'] + ]); + assert.deepStrictEqual(result, { + attempted: true, + released: true, + pids: [2222, 3333] + }); +}); + +test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', () => { + const calls = []; + const killed = []; + const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', { + DEFAULT_WEB_PORT: 3737, + spawnSync(command, args) { + calls.push([command, args]); + if (command === 'lsof') { + return { status: 0, stdout: '9001\n9002\n', stderr: '' }; + } + if (command === 'ps') { + return { + status: 0, + stdout: [ + 'UID PID PPID C STIME TTY TIME CMD', + 'u0_a876 9001 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser', + 'u0_a876 9002 1000 0 1970 ? 00:00:00 /usr/bin/codexmate run', + 'u0_a876 9100 1000 0 1970 ? 00:00:00 node /repo/cli.js config' + ].join('\n'), + stderr: '' + }; + } + throw new Error(`unexpected command: ${command}`); + }, + process: { + platform: 'linux', + pid: 9002, + kill(pid, signal) { + killed.push([pid, signal]); + } + }, + console: { log() {} } + }); + + const result = releaseRunPortIfNeeded(3737, '0.0.0.0'); + assert.deepStrictEqual(calls, [ + ['lsof', ['-ti', 'tcp:3737']], + ['ps', ['-ef']] + ]); + assert.deepStrictEqual(killed, [[9001, 'SIGKILL']]); + assert.deepStrictEqual(result, { + attempted: true, + released: true, + pids: [9001] + }); +}); + +test('releaseRunPortIfNeeded skips ps-based kill when no port-scoped owner pids can be identified', () => { + const calls = []; + const killed = []; + const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', { + DEFAULT_WEB_PORT: 3737, + spawnSync(command, args) { + calls.push([command, args]); + if (command === 'lsof') { + return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' }; + } + if (command === 'fuser') { + return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' }; + } + if (command === 'ps') { + throw new Error('ps should not run without port-scoped pid candidates'); + } + throw new Error(`unexpected command: ${command}`); + }, + process: { + platform: 'linux', + kill(pid, signal) { + killed.push([pid, signal]); + } + }, + console: { log() {} } + }); + + const result = releaseRunPortIfNeeded(3737, '0.0.0.0'); + assert.deepStrictEqual(calls, [ + ['lsof', ['-ti', 'tcp:3737']], + ['fuser', ['3737/tcp']] + ]); + assert.deepStrictEqual(killed, []); + assert.deepStrictEqual(result, { + attempted: true, + released: false, + pids: [] + }); +}); + +test('releaseRunPortIfNeeded only taskkills Windows listeners that match host and managed command line', () => { + const calls = []; + const logs = []; + const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', { + DEFAULT_WEB_PORT: 3737, + spawnSync(command, args) { + calls.push([command, args]); + if (command === 'netstat') { + return { + status: 0, + stdout: [ + ' TCP 0.0.0.0:3737 0.0.0.0:0 LISTENING 1111', + ' TCP 127.0.0.1:3737 0.0.0.0:0 LISTENING 2222', + ' TCP 0.0.0.0:3737 0.0.0.0:0 LISTENING 3333' + ].join('\r\n'), + stderr: '' + }; + } + if (command === 'powershell') { + if (String(args[2]).includes('1111')) { + return { status: 0, stdout: 'node C:\\repo\\cli.js run --no-browser\r\n', stderr: '' }; + } + if (String(args[2]).includes('3333')) { + return { status: 0, stdout: 'node C:\\repo\\other-server.js\r\n', stderr: '' }; + } + throw new Error(`unexpected powershell args: ${args.join(' ')}`); + } + if (command === 'taskkill') { + return { status: 0, stdout: '', stderr: '' }; + } + throw new Error(`unexpected command: ${command}`); + }, + process: { platform: 'win32', pid: 9999 }, + console: { log(message) { logs.push(message); } } + }); + + const result = releaseRunPortIfNeeded(3737, '0.0.0.0'); + assert.deepStrictEqual(calls, [ + ['netstat', ['-ano', '-p', 'tcp']], + ['powershell', ['-NoProfile', '-Command', '$p = Get-CimInstance Win32_Process -Filter "ProcessId = 1111"; if ($p) { $p.CommandLine }']], + ['taskkill', ['/PID', '1111', '/F']], + ['powershell', ['-NoProfile', '-Command', '$p = Get-CimInstance Win32_Process -Filter "ProcessId = 3333"; if ($p) { $p.CommandLine }']] + ]); + assert.deepStrictEqual(result, { + attempted: true, + released: true, + pids: [1111] + }); + assert.deepStrictEqual(logs, ['~ 已释放端口 3737 占用']); +}); + +test('cmdStart releases the resolved port before creating the web server', () => { + const resolveIndex = cmdStartSource.indexOf('resolveWebPort('); + const releaseIndex = cmdStartSource.indexOf('releaseRunPortIfNeeded(port, host)'); + const createIndex = cmdStartSource.indexOf('createWebServer('); + + assert(resolveIndex >= 0, 'cmdStart should resolve the web port'); + assert(releaseIndex >= 0, 'cmdStart should release the run port with host before startup'); + assert(createIndex >= 0, 'cmdStart should create the web server'); + assert(resolveIndex < releaseIndex, 'cmdStart should resolve the port before releasing it'); + assert(releaseIndex < createIndex, 'cmdStart should release the port before creating the web server'); +}); + const getCodexSkillsDirSource = extractFunctionBySignature( cliContent, 'function getCodexSkillsDir() {', diff --git a/tests/unit/web-ui-behavior-parity.test.mjs b/tests/unit/web-ui-behavior-parity.test.mjs index 5182ed1..9c3b0c7 100644 --- a/tests/unit/web-ui-behavior-parity.test.mjs +++ b/tests/unit/web-ui-behavior-parity.test.mjs @@ -105,7 +105,7 @@ function createLoadAllContext() { currentProvider: 'existing-provider', currentModel: 'existing-model', serviceTier: 'fast', - modelReasoningEffort: 'high', + modelReasoningEffort: 'medium', modelContextWindowInput: 'dirty-context', modelAutoCompactTokenLimitInput: 'dirty-limit', editingCodexBudgetField: 'modelContextWindowInput', diff --git a/web-ui/app.js b/web-ui/app.js index 4c4da4f..cc13d0e 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -33,7 +33,7 @@ document.addEventListener('DOMContentLoaded', () => { currentProvider: '', currentModel: '', serviceTier: 'fast', - modelReasoningEffort: 'high', + modelReasoningEffort: 'medium', modelContextWindowInput: String(DEFAULT_MODEL_CONTEXT_WINDOW), modelAutoCompactTokenLimitInput: String(DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT), editingCodexBudgetField: '', diff --git a/web-ui/modules/app.methods.startup-claude.mjs b/web-ui/modules/app.methods.startup-claude.mjs index 1e3afa7..703b8df 100644 --- a/web-ui/modules/app.methods.startup-claude.mjs +++ b/web-ui/modules/app.methods.startup-claude.mjs @@ -40,7 +40,8 @@ export function createStartupClaudeMethods(options = {}) { const effort = typeof statusRes.modelReasoningEffort === 'string' ? statusRes.modelReasoningEffort.trim().toLowerCase() : ''; - this.modelReasoningEffort = effort || 'high'; + const allowedReasoningEfforts = new Set(['low', 'medium', 'high', 'xhigh']); + this.modelReasoningEffort = allowedReasoningEfforts.has(effort) ? effort : 'medium'; } { const contextWindow = this.normalizePositiveIntegerInput( diff --git a/web-ui/partials/index/panel-config-codex.html b/web-ui/partials/index/panel-config-codex.html index 32f8516..0c971e7 100644 --- a/web-ui/partials/index/panel-config-codex.html +++ b/web-ui/partials/index/panel-config-codex.html @@ -78,8 +78,8 @@ 推理强度