diff --git a/src/utils/model/model.auth.test.ts b/src/utils/model/model.auth.test.ts index 561cb6b..64d1de0 100644 --- a/src/utils/model/model.auth.test.ts +++ b/src/utils/model/model.auth.test.ts @@ -6,6 +6,7 @@ import { getDefaultMainLoopModelSetting, getDefaultOpusModel, isOpus1mMergeEnabled, + parseUserSpecifiedModel, } from './model.js' import { GLM_5_2_MODEL } from './ncodeModels.js' @@ -73,6 +74,16 @@ function restoreEnv(): void { } else { process.env.NOUMENA_DEFAULT_HAIKU_MODEL = originalDefaultHaiku } + if (originalOpenAIApiKey === undefined) { + delete process.env.OPENAI_API_KEY + } else { + process.env.OPENAI_API_KEY = originalOpenAIApiKey + } + if (originalOpenAIModel === undefined) { + delete process.env.OPENAI_MODEL + } else { + process.env.OPENAI_MODEL = originalOpenAIModel + } } function makeSession( @@ -270,4 +281,40 @@ describe('model auth session gating', () => { }) }) + it('forwards BYOK OpenAI-compat model ids verbatim, even when they collide with a managed alias', () => { + process.env.CLAUDE_CODE_ENTRYPOINT = 'cli' + process.env.USER_TYPE = 'test' + delete process.env.NCODE_BUILD_MODE + delete process.env.CLAUDE_CODE_USE_BEDROCK + delete process.env.CLAUDE_CODE_USE_VERTEX + delete process.env.CLAUDE_CODE_USE_FOUNDRY + process.env.OPENAI_API_KEY = 'openai-key' + + const session = makeSession({ + principalKind: 'api_key_user', + principalSource: 'direct_api_key_env', + sessionState: 'usable', + headersKind: 'none', + providerAuthKind: 'byok_static_env', + providerPlan: { + mode: 'byok_static_env', + source: 'direct_api_key_env', + staticKeyEnvVarName: 'OPENAI_API_KEY', + }, + hasUsableApiKey: true, + apiKey: 'openai-key', + rawApiKeySource: 'OPENAI_API_KEY', + }) + + withMockCurrentSession(session, () => { + // `glm-5.2` / `k2.7` are reserved NCode managed aliases, but here they name + // a model on the user's own OpenAI-compatible server. They must reach the + // wire verbatim rather than being rewritten to an internal deployment path + // (which the user's server would 404). + expect(parseUserSpecifiedModel('glm-5.2')).toBe('glm-5.2') + expect(parseUserSpecifiedModel('k2.7')).toBe('k2.7') + expect(parseUserSpecifiedModel('my-own-model')).toBe('my-own-model') + }) + }) + }) diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index f76c6a4..6fa2886 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -566,9 +566,16 @@ export function parseUserSpecifiedModel( ? normalizedModel.replace(/\[1m]$/i, '').trim() : normalizedModel - const exactNcodeModel = resolveNCodeManagedModel(normalizedModel) - if (exactNcodeModel) { - return exactNcodeModel.model + // BYOK OpenAI-compat: the model id names a model on the user's own server and + // must pass through verbatim. Never resolve it against the built-in NCODE + // managed-model aliases — an id that collides with a reserved alias (e.g. + // `glm-5.2`, `k2.7`) would otherwise be rewritten to an internal deployment + // path the user's server does not recognize, causing a 404. + if (!isOpenAICompatByokActive()) { + const exactNcodeModel = resolveNCodeManagedModel(normalizedModel) + if (exactNcodeModel) { + return exactNcodeModel.model + } } if (isModelAlias(modelString)) { @@ -587,9 +594,13 @@ export function parseUserSpecifiedModel( } } - const ncodeModel = resolveNCodeManagedModel(modelString) - if (ncodeModel) { - return ncodeModel.model + // See the BYOK note above: managed-alias resolution is skipped entirely for + // BYOK OpenAI-compat sessions so the user's model id is forwarded verbatim. + if (!isOpenAICompatByokActive()) { + const ncodeModel = resolveNCodeManagedModel(modelString) + if (ncodeModel) { + return ncodeModel.model + } } // Opus 4/4.1 are no longer available on the first-party API (same as