From 8733560f268eaab58e5dbdbb1c8ac5c836c62182 Mon Sep 17 00:00:00 2001 From: Sergei Nikitin Date: Thu, 16 Jul 2026 10:34:25 +0400 Subject: [PATCH 1/2] fix(config): prevent haiku model appearing in sonnet slot for haiku-only tenants (EPMCDME-12779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a tenant's global profile sets sonnetModel to the same value as haikuModel (e.g. an SSO-provisioned haiku-only seat), transformEnvVars was mapping it to ANTHROPIC_DEFAULT_SONNET_MODEL, causing the same model ID to appear twice in the /model picker — once under Custom Haiku and once under Custom Sonnet. Fix: skip ANTHROPIC_DEFAULT_SONNET_MODEL when CODEMIE_SONNET_MODEL equals CODEMIE_HAIKU_MODEL, falling back to the opus-routing or haiku-only subagent branch instead. Also always emit CODEMIE_SONNET_MODEL / CODEMIE_OPUS_MODEL from exportProviderEnvVars (empty string when absent) so stale shell values are overridden at env-merge time. Same guard applied to bedrock.template.ts. Generated with AI Co-Authored-By: codemie-ai --- src/agents/core/BaseAgentAdapter.ts | 23 ++++++- .../core/__tests__/model-tier-config.test.ts | 36 ++++++++-- .../model-tier-auto-selection.test.ts | 67 ++++++++++++++++++- src/cli/commands/setup.ts | 22 +++++- .../plugins/bedrock/bedrock.template.ts | 21 +++++- src/utils/config.ts | 9 ++- 6 files changed, 162 insertions(+), 16 deletions(-) diff --git a/src/agents/core/BaseAgentAdapter.ts b/src/agents/core/BaseAgentAdapter.ts index fa6ceb2a..e2efef8e 100644 --- a/src/agents/core/BaseAgentAdapter.ts +++ b/src/agents/core/BaseAgentAdapter.ts @@ -1062,15 +1062,36 @@ export abstract class BaseAgentAdapter implements AgentAdapter { } // Transform model tiers (haiku/sonnet/opus) + if (envMapping.haikuModel) { + for (const envVar of envMapping.haikuModel) delete env[envVar]; + } + if (envMapping.sonnetModel) { + for (const envVar of envMapping.sonnetModel) delete env[envVar]; + } + if (envMapping.opusModel) { + for (const envVar of envMapping.opusModel) delete env[envVar]; + } + if (env.CODEMIE_HAIKU_MODEL && envMapping.haikuModel) { for (const envVar of envMapping.haikuModel) { env[envVar] = env.CODEMIE_HAIKU_MODEL; } } - if (env.CODEMIE_SONNET_MODEL && envMapping.sonnetModel) { + if (env.CODEMIE_SONNET_MODEL && env.CODEMIE_SONNET_MODEL !== env.CODEMIE_HAIKU_MODEL && envMapping.sonnetModel) { + // Distinct sonnet tier — map to all target vars normally for (const envVar of envMapping.sonnetModel) { env[envVar] = env.CODEMIE_SONNET_MODEL; } + } else if ((!env.CODEMIE_SONNET_MODEL || env.CODEMIE_SONNET_MODEL === env.CODEMIE_HAIKU_MODEL) && env.CODEMIE_OPUS_MODEL && envMapping.sonnetModel?.includes('CLAUDE_CODE_SUBAGENT_MODEL')) { + // No distinct sonnet tier, opus provisioned: route subagent to opus. + // ANTHROPIC_DEFAULT_SONNET_MODEL is intentionally left unset to prevent + // duplicate-ID display in /model (EPMCDME-12779). + env['CLAUDE_CODE_SUBAGENT_MODEL'] = env.CODEMIE_OPUS_MODEL; + } else if ((!env.CODEMIE_SONNET_MODEL || env.CODEMIE_SONNET_MODEL === env.CODEMIE_HAIKU_MODEL) && !env.CODEMIE_OPUS_MODEL && env.CODEMIE_HAIKU_MODEL && envMapping.sonnetModel?.includes('CLAUDE_CODE_SUBAGENT_MODEL')) { + // Haiku-only tenant: route subagent to haiku. + // ANTHROPIC_DEFAULT_SONNET_MODEL is intentionally left unset to prevent + // duplicate-ID display in /model (EPMCDME-12779). + env['CLAUDE_CODE_SUBAGENT_MODEL'] = env.CODEMIE_HAIKU_MODEL; } if (env.CODEMIE_OPUS_MODEL && envMapping.opusModel) { for (const envVar of envMapping.opusModel) { diff --git a/src/agents/core/__tests__/model-tier-config.test.ts b/src/agents/core/__tests__/model-tier-config.test.ts index 5c8cd31d..eedb3856 100644 --- a/src/agents/core/__tests__/model-tier-config.test.ts +++ b/src/agents/core/__tests__/model-tier-config.test.ts @@ -64,9 +64,10 @@ describe('Model Tier Configuration', () => { vi.restoreAllMocks(); }); - it('should transform CODEMIE_HAIKU_MODEL to ANTHROPIC_DEFAULT_HAIKU_MODEL', () => { + it('should transform CODEMIE_HAIKU_MODEL to ANTHROPIC_DEFAULT_HAIKU_MODEL when sonnet is also present', () => { const env: NodeJS.ProcessEnv = { CODEMIE_HAIKU_MODEL: 'claude-haiku-4-5-20251001', + CODEMIE_SONNET_MODEL: 'claude-sonnet-4-6', // not haiku-only → normal mapping applies }; const result = adapter.testTransformEnvVars(env); @@ -161,7 +162,7 @@ describe('Model Tier Configuration', () => { expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBeUndefined(); }); - it('should handle partial tier configuration', () => { + it('should handle haiku-only configuration by mapping haiku to its slot and CLAUDE_CODE_SUBAGENT_MODEL (EPMCDME-12779)', () => { const env: NodeJS.ProcessEnv = { CODEMIE_HAIKU_MODEL: 'claude-haiku-4-5-20251001', // sonnetModel and opusModel not provided @@ -169,9 +170,31 @@ describe('Model Tier Configuration', () => { const result = adapter.testTransformEnvVars(env); - // Only haiku should be set + // Haiku-only: set ANTHROPIC_DEFAULT_HAIKU_MODEL normally; ANTHROPIC_DEFAULT_SONNET_MODEL + // is intentionally left unset. CLAUDE_CODE_SUBAGENT_MODEL routes background tasks to haiku. expect(result.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5-20251001'); expect(result.ANTHROPIC_DEFAULT_SONNET_MODEL).toBeUndefined(); + expect(result.CLAUDE_CODE_SUBAGENT_MODEL).toBe('claude-haiku-4-5-20251001'); + expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBeUndefined(); + }); + + it('should clear stale sonnet/opus vars when switching to haiku-only config (EPMCDME-12779)', () => { + // Simulates the user removing sonnet/opus from config after a prior session that had + // all three tiers — stale ANTHROPIC_DEFAULT_SONNET_MODEL and ANTHROPIC_DEFAULT_OPUS_MODEL + // must not survive. + const env: NodeJS.ProcessEnv = { + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6-20260205', + CODEMIE_HAIKU_MODEL: 'claude-haiku-4-5-20251001', + // CODEMIE_SONNET_MODEL and CODEMIE_OPUS_MODEL absent — haiku-only + }; + + const result = adapter.testTransformEnvVars(env); + + expect(result.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5-20251001'); + expect(result.ANTHROPIC_DEFAULT_SONNET_MODEL).toBeUndefined(); + expect(result.CLAUDE_CODE_SUBAGENT_MODEL).toBe('claude-haiku-4-5-20251001'); expect(result.ANTHROPIC_DEFAULT_OPUS_MODEL).toBeUndefined(); }); @@ -236,9 +259,10 @@ describe('ConfigLoader.exportProviderEnvVars', () => { const env = ConfigLoader.exportProviderEnvVars(config); expect(env.CODEMIE_MODEL).toBe('claude-4-5-sonnet'); - expect(env.CODEMIE_HAIKU_MODEL).toBeUndefined(); - expect(env.CODEMIE_SONNET_MODEL).toBeUndefined(); - expect(env.CODEMIE_OPUS_MODEL).toBeUndefined(); + // Always emitted as empty string to override stale shell values (EPMCDME-12779) + expect(env.CODEMIE_HAIKU_MODEL).toBe(''); + expect(env.CODEMIE_SONNET_MODEL).toBe(''); + expect(env.CODEMIE_OPUS_MODEL).toBe(''); }); it('should not export placeholder auth token for anthropic-subscription', async () => { diff --git a/src/cli/commands/__tests__/model-tier-auto-selection.test.ts b/src/cli/commands/__tests__/model-tier-auto-selection.test.ts index ba555ab3..a689726c 100644 --- a/src/cli/commands/__tests__/model-tier-auto-selection.test.ts +++ b/src/cli/commands/__tests__/model-tier-auto-selection.test.ts @@ -9,7 +9,8 @@ * - Environment variable priority */ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { autoSelectModelTiers } from '../setup.js'; // Import the functions we want to test // We'll need to export them from setup.ts for testing @@ -382,3 +383,67 @@ describe('autoSelectModelTiers integration', () => { }); }); }); + +describe('autoSelectModelTiers — opus-only tenant (EPMCDME-12779)', () => { + beforeEach(() => { + vi.stubEnv('ANTHROPIC_DEFAULT_HAIKU_MODEL', ''); + vi.stubEnv('ANTHROPIC_DEFAULT_SONNET_MODEL', ''); + vi.stubEnv('ANTHROPIC_DEFAULT_OPUS_MODEL', ''); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('should not set sonnetModel when selectedModel is opus-class', async () => { + const models = ['claude-opus-4-6-20260205']; + const result = await autoSelectModelTiers(models, 'claude-opus-4-6-20260205'); + expect(result.sonnetModel).toBeUndefined(); + expect(result.opusModel).toBe('claude-opus-4-6-20260205'); + }); + + it('should not set sonnetModel when selectedModel contains opus keyword', async () => { + const models = ['claude-opus-4-7', 'claude-haiku-4-5-20251001']; + const result = await autoSelectModelTiers(models, 'claude-opus-4-7'); + expect(result.sonnetModel).toBeUndefined(); + expect(result.opusModel).toBe('claude-opus-4-7'); + expect(result.haikuModel).toBe('claude-haiku-4-5-20251001'); + }); + + it('should set sonnetModel normally when selectedModel is sonnet-class', async () => { + const models = ['claude-sonnet-4-6', 'claude-opus-4-6-20260205', 'claude-haiku-4-5-20251001']; + const result = await autoSelectModelTiers(models, 'claude-sonnet-4-6'); + expect(result.sonnetModel).toBe('claude-sonnet-4-6'); + expect(result.opusModel).toBe('claude-opus-4-6-20260205'); + expect(result.haikuModel).toBe('claude-haiku-4-5-20251001'); + }); + + it('should not set sonnetModel when selectedModel is a custom/unknown model ID', async () => { + const models = ['my-enterprise-llm', 'claude-haiku-4-5-20251001']; + const result = await autoSelectModelTiers(models, 'my-enterprise-llm'); + expect(result.sonnetModel).toBeUndefined(); + expect(result.haikuModel).toBe('claude-haiku-4-5-20251001'); + }); + + it('should auto-select sonnet from models list when selectedModel is haiku-class (EPMCDME-12779)', async () => { + // User selects haiku as primary but tenant also has sonnet provisioned. + // sonnetModel must be assigned from the list so Claude Code's Custom Sonnet slot + // shows the real sonnet model instead of falling back to the selected haiku model. + const models = ['claude-haiku-4-5-20251001', 'claude-sonnet-4-6', 'claude-opus-4-6-20260205']; + const result = await autoSelectModelTiers(models, 'claude-haiku-4-5-20251001'); + expect(result.sonnetModel).toBe('claude-sonnet-4-6'); + expect(result.haikuModel).toBe('claude-haiku-4-5-20251001'); + expect(result.opusModel).toBe('claude-opus-4-6-20260205'); + }); + + it('should not set sonnetModel when no sonnet model is in the available list', async () => { + // True no-sonnet tenant: only haiku and opus provisioned. + // sonnetModel must remain undefined so the haiku-only/opus-only fallback in + // BaseAgentAdapter can handle the slot routing correctly. + const models = ['claude-haiku-4-5-20251001', 'claude-opus-4-6-20260205']; + const result = await autoSelectModelTiers(models, 'claude-haiku-4-5-20251001'); + expect(result.sonnetModel).toBeUndefined(); + expect(result.haikuModel).toBe('claude-haiku-4-5-20251001'); + expect(result.opusModel).toBe('claude-opus-4-6-20260205'); + }); +}); diff --git a/src/cli/commands/setup.ts b/src/cli/commands/setup.ts index cbefb713..08cb3ecf 100644 --- a/src/cli/commands/setup.ts +++ b/src/cli/commands/setup.ts @@ -574,7 +574,7 @@ function compareModelVersions(a: string, b: string): number { * * Latest = highest version number parsed from model name */ -async function autoSelectModelTiers( +export async function autoSelectModelTiers( models: string[], selectedModel: string ): Promise<{ haikuModel?: string; sonnetModel?: string; opusModel?: string }> { @@ -602,6 +602,7 @@ async function autoSelectModelTiers( // Filter models by type const haikuModels = models.filter(m => m.toLowerCase().includes('haiku')); + const sonnetModels = models.filter(m => m.toLowerCase().includes('sonnet')); const opusModels = models.filter(m => m.toLowerCase().includes('opus')); // Select latest haiku model (or use env var if set) @@ -621,13 +622,28 @@ async function autoSelectModelTiers( }); } - // Use selected model as sonnet tier (or env var if set) + // Select sonnet model: prefer env var, then selectedModel if it's sonnet-class, then + // auto-select the latest sonnet model from the available list (same pattern as haiku/opus). + // This ensures the Custom Sonnet slot in Claude Code's /model picker is always populated + // with a real sonnet model when one is provisioned, regardless of which tier the user + // selected as their primary model (EPMCDME-12779). if (envSonnet) { result.sonnetModel = envSonnet; logger.debug('Using sonnet model from environment variable', { model: envSonnet }); - } else { + } else if (selectedModel.toLowerCase().includes('sonnet')) { result.sonnetModel = selectedModel; logger.debug('Using selected model as sonnet tier', { model: selectedModel }); + } else if (sonnetModels.length > 0) { + const sortedSonnet = [...sonnetModels].sort((a, b) => compareModelVersions(b, a)); + const latestSonnet = sortedSonnet[0]; + result.sonnetModel = latestSonnet; + logger.debug('Auto-selected sonnet model', { + selected: latestSonnet, + candidates: sonnetModels, + sortedOrder: sortedSonnet + }); + } else { + logger.debug('No sonnet model available — sonnet tier not assigned', { availableModels: models }); } // Select latest opus model (or use env var if set) diff --git a/src/providers/plugins/bedrock/bedrock.template.ts b/src/providers/plugins/bedrock/bedrock.template.ts index e6d6f675..59d9c213 100644 --- a/src/providers/plugins/bedrock/bedrock.template.ts +++ b/src/providers/plugins/bedrock/bedrock.template.ts @@ -109,13 +109,30 @@ export const BedrockTemplate = registerProvider({ } // Model tier configuration for Bedrock - // Maps CodeMie tier models to Claude Code environment variables + // Maps CodeMie tier models to Claude Code environment variables. + // Clear stale values first so haiku-only / opus-only tenants don't inherit + // vars from a prior process and show duplicates in /model (EPMCDME-12779). + delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL; + delete env.ANTHROPIC_DEFAULT_SONNET_MODEL; + delete env.ANTHROPIC_DEFAULT_OPUS_MODEL; + delete env.CLAUDE_CODE_SUBAGENT_MODEL; if (env.CODEMIE_HAIKU_MODEL) { env.ANTHROPIC_DEFAULT_HAIKU_MODEL = env.CODEMIE_HAIKU_MODEL; } - if (env.CODEMIE_SONNET_MODEL) { + if (env.CODEMIE_SONNET_MODEL && env.CODEMIE_SONNET_MODEL !== env.CODEMIE_HAIKU_MODEL) { env.ANTHROPIC_DEFAULT_SONNET_MODEL = env.CODEMIE_SONNET_MODEL; env.CLAUDE_CODE_SUBAGENT_MODEL = env.CODEMIE_SONNET_MODEL; + } else if (env.CODEMIE_OPUS_MODEL) { + // Opus-only tenant: route subagent to opus; ANTHROPIC_DEFAULT_SONNET_MODEL is + // intentionally left unset to prevent duplicate-ID display (EPMCDME-12779 FR-002). + env.CLAUDE_CODE_SUBAGENT_MODEL = env.CODEMIE_OPUS_MODEL; + } else if (env.CODEMIE_HAIKU_MODEL) { + // Haiku-only tenant: set CLAUDE_CODE_SUBAGENT_MODEL so background tasks use the + // provisioned model. ANTHROPIC_DEFAULT_SONNET_MODEL is intentionally left unset. + // Routing haiku through the sonnet slot caused a duplicate because Claude Code + // shows its built-in haiku default even when ANTHROPIC_DEFAULT_HAIKU_MODEL is not + // set (EPMCDME-12779). + env.CLAUDE_CODE_SUBAGENT_MODEL = env.CODEMIE_HAIKU_MODEL; } if (env.CODEMIE_OPUS_MODEL) { env.ANTHROPIC_DEFAULT_OPUS_MODEL = env.CODEMIE_OPUS_MODEL; diff --git a/src/utils/config.ts b/src/utils/config.ts index 741d32e8..5564fff7 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1387,9 +1387,12 @@ export class ConfigLoader { if (config.model) env.CODEMIE_MODEL = config.model; if (config.reasoningEffort) env.CODEMIE_REASONING_EFFORT = config.reasoningEffort; - if (config.haikuModel) env.CODEMIE_HAIKU_MODEL = config.haikuModel; - if (config.sonnetModel) env.CODEMIE_SONNET_MODEL = config.sonnetModel; - if (config.opusModel) env.CODEMIE_OPUS_MODEL = config.opusModel; + // Always emit tier model vars — even when absent — so stale shell values are + // overridden during env merge in BaseAgentAdapter (EPMCDME-12779). + // Empty string is falsy, so transformEnvVars() correctly skips absent tiers. + env.CODEMIE_HAIKU_MODEL = config.haikuModel ?? ''; + env.CODEMIE_SONNET_MODEL = config.sonnetModel ?? ''; + env.CODEMIE_OPUS_MODEL = config.opusModel ?? ''; if (config.timeout) env.CODEMIE_TIMEOUT = String(config.timeout); if (config.debug) env.CODEMIE_DEBUG = String(config.debug); From 065f5f1fce3af14dd62db8d5def5a040de17aa4a Mon Sep 17 00:00:00 2001 From: SergeyVNikitin Date: Thu, 23 Jul 2026 20:01:08 +0400 Subject: [PATCH 2/2] fix(config): validate stale env vars and remove dead code (EPMCDME-12779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **CR-002 [CRITICAL]** — autoSelectModelTiers early-return bypassed validation - Removed early-return that checked if all three ANTHROPIC_DEFAULT_*_MODEL vars were set, which prevented the new auto-selection validation logic from running - Added validation checks to verify env vars are in the current models list before trusting them (haiku, sonnet, opus) - Falls through to auto-selection if env vars are stale or invalid - Fixes: users running setup repeatedly now get correct model assignments even with stale env vars from previous sessions **CR-004 [MAJOR]** — removed redundant delete blocks in transformEnvVars - Deleted three dead delete blocks (haikuModel, sonnetModel, opusModel) that were already cleared in Step 1 of transformEnvVars - Added clarifying comment to document the dependency on Step 1 - Prevents future maintenance drift between Step 1 and tier-routing block Generated with AI Co-Authored-By: codemie-ai Claude-Session: https://claude.ai/code/session_01Qp7pTNUw74uh3J7CFQaneq --- src/agents/core/BaseAgentAdapter.ts | 11 +--------- src/cli/commands/setup.ts | 34 +++++++++-------------------- 2 files changed, 11 insertions(+), 34 deletions(-) diff --git a/src/agents/core/BaseAgentAdapter.ts b/src/agents/core/BaseAgentAdapter.ts index e2efef8e..cc13756e 100644 --- a/src/agents/core/BaseAgentAdapter.ts +++ b/src/agents/core/BaseAgentAdapter.ts @@ -1062,16 +1062,7 @@ export abstract class BaseAgentAdapter implements AgentAdapter { } // Transform model tiers (haiku/sonnet/opus) - if (envMapping.haikuModel) { - for (const envVar of envMapping.haikuModel) delete env[envVar]; - } - if (envMapping.sonnetModel) { - for (const envVar of envMapping.sonnetModel) delete env[envVar]; - } - if (envMapping.opusModel) { - for (const envVar of envMapping.opusModel) delete env[envVar]; - } - + // Note: All tier vars were already cleared in Step 1 above if (env.CODEMIE_HAIKU_MODEL && envMapping.haikuModel) { for (const envVar of envMapping.haikuModel) { env[envVar] = env.CODEMIE_HAIKU_MODEL; diff --git a/src/cli/commands/setup.ts b/src/cli/commands/setup.ts index 08cb3ecf..4845292d 100644 --- a/src/cli/commands/setup.ts +++ b/src/cli/commands/setup.ts @@ -296,9 +296,9 @@ async function handlePluginSetup( } // Merge model tiers into config - if (modelTiers.haikuModel) config.haikuModel = modelTiers.haikuModel; - if (modelTiers.sonnetModel) config.sonnetModel = modelTiers.sonnetModel; - if (modelTiers.opusModel) config.opusModel = modelTiers.opusModel; + config.haikuModel = modelTiers.haikuModel; + config.sonnetModel = modelTiers.sonnetModel; + config.opusModel = modelTiers.opusModel; // Step 5: Ask for profile name (if creating new) let finalProfileName = profileName; @@ -583,21 +583,7 @@ export async function autoSelectModelTiers( const envSonnet = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL; const envOpus = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL; - // If all env vars are set, use those - if (envHaiku && envSonnet && envOpus) { - logger.debug('Using model tiers from environment variables', { - haiku: envHaiku, - sonnet: envSonnet, - opus: envOpus - }); - return { - haikuModel: envHaiku, - sonnetModel: envSonnet, - opusModel: envOpus - }; - } - - // Otherwise, auto-select from available models + // Auto-select from available models with validation against env vars const result: { haikuModel?: string; sonnetModel?: string; opusModel?: string } = {}; // Filter models by type @@ -605,8 +591,8 @@ export async function autoSelectModelTiers( const sonnetModels = models.filter(m => m.toLowerCase().includes('sonnet')); const opusModels = models.filter(m => m.toLowerCase().includes('opus')); - // Select latest haiku model (or use env var if set) - if (envHaiku) { + // Select latest haiku model (or use env var if set and valid) + if (envHaiku && haikuModels.includes(envHaiku)) { result.haikuModel = envHaiku; logger.debug('Using haiku model from environment variable', { model: envHaiku }); } else if (haikuModels.length > 0) { @@ -622,12 +608,12 @@ export async function autoSelectModelTiers( }); } - // Select sonnet model: prefer env var, then selectedModel if it's sonnet-class, then + // Select sonnet model: prefer env var (if valid), then selectedModel if it's sonnet-class, then // auto-select the latest sonnet model from the available list (same pattern as haiku/opus). // This ensures the Custom Sonnet slot in Claude Code's /model picker is always populated // with a real sonnet model when one is provisioned, regardless of which tier the user // selected as their primary model (EPMCDME-12779). - if (envSonnet) { + if (envSonnet && sonnetModels.includes(envSonnet)) { result.sonnetModel = envSonnet; logger.debug('Using sonnet model from environment variable', { model: envSonnet }); } else if (selectedModel.toLowerCase().includes('sonnet')) { @@ -646,8 +632,8 @@ export async function autoSelectModelTiers( logger.debug('No sonnet model available — sonnet tier not assigned', { availableModels: models }); } - // Select latest opus model (or use env var if set) - if (envOpus) { + // Select latest opus model (or use env var if set and valid) + if (envOpus && opusModels.includes(envOpus)) { result.opusModel = envOpus; logger.debug('Using opus model from environment variable', { model: envOpus }); } else if (opusModels.length > 0) {