From 458120889f6ce54cc9f0b287461d5e38eae48a20 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Wed, 22 Apr 2026 09:01:44 +0800 Subject: [PATCH] fix(model): codex/nvidia-nim/minimax now read OPENAI_MODEL env (#815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getUserSpecifiedModelSetting() decides which env var to consult based on the active provider. The check included openai and github but omitted codex, nvidia-nim, and minimax — even though all three use the OpenAI shim transport and get their model routing via CLAUDE_CODE_USE_OPENAI=1 + OPENAI_MODEL (set by applyProviderProfileToProcessEnv). Concrete failure: user switches from Moonshot profile (which persisted settings.model='kimi-k2.6') to the Codex profile. The new profile correctly writes OPENAI_MODEL=codexplan + base URL to chatgpt.com/backend-api/codex. Startup banner reflects Codex / gpt-5.4 correctly. But at request time getUserSpecifiedModelSetting() returns early for provider='codex' (not in the env-consult list), falls through to the stale settings.model='kimi-k2.6', and the Codex API rejects: API Error 400: "The 'kimi-k2.6' model is not supported when using Codex with a ChatGPT account." Fix: extract an isOpenAIShimProvider flag covering openai|codex|github| nvidia-nim|minimax — all providers that set OPENAI_MODEL as their model env var. The Gemini and Mistral branches stay as-is (they use GEMINI_MODEL / MISTRAL_MODEL). Five regression tests pin the fix for each OpenAI-shim provider plus guard tests for openai and github that already worked. Co-authored-by: OpenClaude --- .../model/model.openai-shim-providers.test.ts | 115 ++++++++++++++++++ src/utils/model/model.ts | 15 ++- 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/utils/model/model.openai-shim-providers.test.ts diff --git a/src/utils/model/model.openai-shim-providers.test.ts b/src/utils/model/model.openai-shim-providers.test.ts new file mode 100644 index 00000000..9b6c2ae4 --- /dev/null +++ b/src/utils/model/model.openai-shim-providers.test.ts @@ -0,0 +1,115 @@ +import { afterEach, beforeEach, expect, test } from 'bun:test' + +import { saveGlobalConfig } from '../config.js' +import { getUserSpecifiedModelSetting } from './model.js' + +const SAVED_ENV = { + CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, + CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB, + CLAUDE_CODE_USE_MISTRAL: process.env.CLAUDE_CODE_USE_MISTRAL, + CLAUDE_CODE_USE_BEDROCK: process.env.CLAUDE_CODE_USE_BEDROCK, + CLAUDE_CODE_USE_VERTEX: process.env.CLAUDE_CODE_USE_VERTEX, + CLAUDE_CODE_USE_FOUNDRY: process.env.CLAUDE_CODE_USE_FOUNDRY, + NVIDIA_NIM: process.env.NVIDIA_NIM, + MINIMAX_API_KEY: process.env.MINIMAX_API_KEY, + OPENAI_MODEL: process.env.OPENAI_MODEL, + OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, + CODEX_API_KEY: process.env.CODEX_API_KEY, + CHATGPT_ACCOUNT_ID: process.env.CHATGPT_ACCOUNT_ID, +} + +function restoreEnv(key: keyof typeof SAVED_ENV): void { + if (SAVED_ENV[key] === undefined) { + delete process.env[key] + } else { + process.env[key] = SAVED_ENV[key] + } +} + +beforeEach(() => { + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_USE_GITHUB + delete process.env.CLAUDE_CODE_USE_MISTRAL + delete process.env.CLAUDE_CODE_USE_BEDROCK + delete process.env.CLAUDE_CODE_USE_VERTEX + delete process.env.CLAUDE_CODE_USE_FOUNDRY + delete process.env.NVIDIA_NIM + delete process.env.MINIMAX_API_KEY + delete process.env.OPENAI_MODEL + delete process.env.OPENAI_BASE_URL + delete process.env.CODEX_API_KEY + delete process.env.CHATGPT_ACCOUNT_ID + saveGlobalConfig(current => ({ + ...current, + model: undefined, + })) +}) + +afterEach(() => { + for (const key of Object.keys(SAVED_ENV) as Array) { + restoreEnv(key) + } + saveGlobalConfig(current => ({ + ...current, + model: undefined, + })) +}) + +test('codex provider reads OPENAI_MODEL, not stale settings.model', () => { + // Regression: switching from Moonshot (settings.model='kimi-k2.6' persisted + // from that session) to the Codex profile. Codex profile correctly sets + // OPENAI_MODEL=codexplan + base URL to chatgpt.com/backend-api/codex. + // getUserSpecifiedModelSetting previously ignored env for 'codex' provider + // and returned settings.model='kimi-k2.6', causing Codex's API to reject + // the request: "The 'kimi-k2.6' model is not supported when using Codex". + saveGlobalConfig(current => ({ ...current, model: 'kimi-k2.6' })) + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://chatgpt.com/backend-api/codex' + process.env.OPENAI_MODEL = 'codexplan' + process.env.CODEX_API_KEY = 'codex-test' + process.env.CHATGPT_ACCOUNT_ID = 'acct_test' + + const model = getUserSpecifiedModelSetting() + expect(model).toBe('codexplan') +}) + +test('nvidia-nim provider reads OPENAI_MODEL, not stale settings.model', () => { + saveGlobalConfig(current => ({ ...current, model: 'kimi-k2.6' })) + process.env.NVIDIA_NIM = '1' + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_MODEL = 'nvidia/llama-3.1-nemotron-70b-instruct' + + const model = getUserSpecifiedModelSetting() + expect(model).toBe('nvidia/llama-3.1-nemotron-70b-instruct') +}) + +test('minimax provider reads OPENAI_MODEL, not stale settings.model', () => { + saveGlobalConfig(current => ({ ...current, model: 'kimi-k2.6' })) + process.env.MINIMAX_API_KEY = 'minimax-test' + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_MODEL = 'MiniMax-M2.5' + + const model = getUserSpecifiedModelSetting() + expect(model).toBe('MiniMax-M2.5') +}) + +test('openai provider still reads OPENAI_MODEL (regression guard)', () => { + saveGlobalConfig(current => ({ ...current, model: 'stale-default' })) + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_MODEL = 'gpt-4o' + + const model = getUserSpecifiedModelSetting() + expect(model).toBe('gpt-4o') +}) + +test('github provider still reads OPENAI_MODEL (regression guard)', () => { + saveGlobalConfig(current => ({ ...current, model: 'stale-default' })) + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_MODEL = 'github:copilot' + + const model = getUserSpecifiedModelSetting() + expect(model).toBe('github:copilot') +}) + diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index eb11c432..9ff9a0c3 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -91,11 +91,24 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined { const setting = normalizeModelSetting(settings.model) // Read the model env var that matches the active provider to prevent // cross-provider leaks (e.g. ANTHROPIC_MODEL sent to the OpenAI API). + // + // All OpenAI-shim providers (openai, codex, github, nvidia-nim, minimax) + // set CLAUDE_CODE_USE_OPENAI=1 + OPENAI_MODEL via + // applyProviderProfileToProcessEnv. Earlier this check only included + // openai/github — codex/nvidia-nim/minimax fell through to the stale + // settings.model, so switching from (say) Moonshot to Codex kept firing + // `kimi-k2.6` at the Codex endpoint and getting 400s. const provider = getAPIProvider() + const isOpenAIShimProvider = + provider === 'openai' || + provider === 'codex' || + provider === 'github' || + provider === 'nvidia-nim' || + provider === 'minimax' specifiedModel = (provider === 'gemini' ? process.env.GEMINI_MODEL : undefined) || (provider === 'mistral' ? process.env.MISTRAL_MODEL : undefined) || - (provider === 'openai' || provider === 'gemini' || provider === 'mistral' || provider === 'github' ? process.env.OPENAI_MODEL : undefined) || + (isOpenAIShimProvider ? process.env.OPENAI_MODEL : undefined) || (provider === 'firstParty' ? process.env.ANTHROPIC_MODEL : undefined) || setting || undefined