Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions src/commands/model/model.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const originalEnv = {
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY,
OPENAI_MODEL: process.env.OPENAI_MODEL,
ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS,
CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED:
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED,
CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID:
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID,
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC,
}
Expand Down Expand Up @@ -74,6 +78,14 @@ afterEach(() => {
restoreEnv('OPENROUTER_API_KEY', originalEnv.OPENROUTER_API_KEY)
restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL)
restoreEnv('ANTHROPIC_CUSTOM_HEADERS', originalEnv.ANTHROPIC_CUSTOM_HEADERS)
restoreEnv(
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED',
originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED,
)
restoreEnv(
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID',
originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID,
)
restoreEnv(
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
originalEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC,
Expand Down Expand Up @@ -159,6 +171,7 @@ test('opens the model picker without awaiting descriptor-backed route refresh',
mock.module('../../utils/providerProfiles.js', () => ({
getActiveOpenAIModelOptionsCache: () => [],
getActiveProviderProfile: () => null,
getProfileModelOptions: () => [],
setActiveOpenAIModelOptionsCache: () => {},
}))

Expand Down Expand Up @@ -210,6 +223,175 @@ test('shouldAutoRefreshRouteCatalog respects discovery refresh modes', async ()
).toBe(false)
})

test('descriptor model options include active profile configured models', async () => {
const activeProfile = {
id: 'mistral-profile',
name: 'Mistral AI',
provider: 'mistral',
baseUrl: 'https://api.mistral.ai/v1',
model: 'devstral-latest, mistral-medium-latest',
apiKey: 'sk-mistral',
}
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED = '1'
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID = activeProfile.id

mock.module('../../utils/providerProfiles.js', () => ({
getActiveOpenAIModelOptionsCache: () => [],
getActiveProviderProfile: () => activeProfile,
getProfileModelOptions: () => [
{
value: 'devstral-latest',
label: 'devstral-latest',
description: 'Provider: Mistral AI',
},
{
value: 'mistral-medium-latest',
label: 'mistral-medium-latest',
description: 'Provider: Mistral AI',
},
],
setActiveOpenAIModelOptionsCache: () => {},
}))

const { mergeActiveProfileModelOptions } =
await importFreshModelModule('descriptor-profile-model-merge')

expect(
mergeActiveProfileModelOptions(
'mistral',
[
{
value: 'devstral-latest',
label: 'Devstral Latest',
description: 'Recommended · Provider: Mistral AI',
},
],
),
).toEqual([
{
value: 'devstral-latest',
label: 'Devstral Latest',
description: 'Recommended · Provider: Mistral AI',
},
{
value: 'mistral-medium-latest',
label: 'mistral-medium-latest',
description: 'Provider: Mistral AI',
},
])
})

test('descriptor model options omit route defaults outside active profile models', async () => {
const activeProfile = {
id: 'mistral-profile',
name: 'Mistral AI',
provider: 'mistral',
baseUrl: 'https://api.mistral.ai/v1',
model: 'mistral-medium-latest, mistral-small-latest',
apiKey: 'sk-mistral',
}
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED = '1'
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID = activeProfile.id

mock.module('../../utils/providerProfiles.js', () => ({
getActiveOpenAIModelOptionsCache: () => [],
getActiveProviderProfile: () => activeProfile,
getProfileModelOptions: () => [
{
value: 'mistral-medium-latest',
label: 'mistral-medium-latest',
description: 'Provider: Mistral AI',
},
{
value: 'mistral-small-latest',
label: 'mistral-small-latest',
description: 'Provider: Mistral AI',
},
],
setActiveOpenAIModelOptionsCache: () => {},
}))

const { mergeActiveProfileModelOptions } =
await importFreshModelModule('descriptor-profile-model-filter')

expect(
mergeActiveProfileModelOptions(
'mistral',
[
{
value: 'devstral-latest',
label: 'Devstral Latest',
description: 'Recommended · Provider: Mistral AI',
},
{
value: 'mistral-small-latest',
label: 'Mistral Small Latest',
description: 'Provider: Mistral AI',
},
],
),
).toEqual([
{
value: 'mistral-medium-latest',
label: 'mistral-medium-latest',
description: 'Provider: Mistral AI',
},
{
value: 'mistral-small-latest',
label: 'Mistral Small Latest',
description: 'Provider: Mistral AI',
},
])
})

test('descriptor model options skip saved profile models for env-selected routes', async () => {
const savedProfile = {
id: 'mistral-profile',
name: 'Mistral AI',
provider: 'mistral',
baseUrl: 'https://api.mistral.ai/v1',
model: 'devstral-latest, mistral-medium-latest',
apiKey: 'sk-mistral',
}
delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED
delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID

mock.module('../../utils/providerProfiles.js', () => ({
getActiveOpenAIModelOptionsCache: () => [],
getActiveProviderProfile: () => savedProfile,
getProfileModelOptions: () => [
{
value: 'mistral-medium-latest',
label: 'mistral-medium-latest',
description: 'Provider: Mistral AI',
},
],
setActiveOpenAIModelOptionsCache: () => {},
}))

const { mergeActiveProfileModelOptions } =
await importFreshModelModule('descriptor-profile-model-env-skip')

expect(
mergeActiveProfileModelOptions(
'openrouter',
[
{
value: 'openai/gpt-5-mini',
label: 'GPT-5 Mini',
description: 'Provider: OpenRouter',
},
],
),
).toEqual([
{
value: 'openai/gpt-5-mini',
label: 'GPT-5 Mini',
description: 'Provider: OpenRouter',
},
])
})

test('/model refresh clears descriptor cache and reports updates', async () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1'
Expand Down Expand Up @@ -264,6 +446,7 @@ test('/model refresh clears descriptor cache and reports updates', async () => {
mock.module('../../utils/providerProfiles.js', () => ({
getActiveOpenAIModelOptionsCache: () => [],
getActiveProviderProfile: () => null,
getProfileModelOptions: () => [],
setActiveOpenAIModelOptionsCache: () => {},
}))

Expand Down Expand Up @@ -329,6 +512,7 @@ test('/model does not auto-refresh descriptor models when nonessential traffic i
mock.module('../../utils/providerProfiles.js', () => ({
getActiveOpenAIModelOptionsCache: () => [],
getActiveProviderProfile: () => null,
getProfileModelOptions: () => [],
setActiveOpenAIModelOptionsCache: () => {},
}))

Expand Down
98 changes: 80 additions & 18 deletions src/commands/model/model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import {
getRouteDescriptor,
resolveRouteCredentialValue,
resolveActiveRouteIdFromEnv,
resolveRouteIdFromBaseUrl,
} from '../../integrations/routeMetadata.js'
import { resolveProfileRoute } from '../../integrations/profileResolver.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
Expand Down Expand Up @@ -62,6 +64,7 @@ import { parseCustomHeadersEnv } from '../../utils/providerCustomHeaders.js'
import {
getActiveOpenAIModelOptionsCache,
getActiveProviderProfile,
getProfileModelOptions,
setActiveOpenAIModelOptionsCache,
} from '../../utils/providerProfiles.js'

Expand Down Expand Up @@ -108,6 +111,55 @@ function haveSameModelOptions(left: ModelOption[], right: ModelOption[]): boolea
})
}

export function mergeActiveProfileModelOptions(
routeId: string,
routeOptions: ModelOption[],
): ModelOption[] {
const activeProfile = getActiveProviderProfile()
if (!activeProfile) {
return routeOptions
}

const profileEnvApplied =
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED === '1' &&
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID === activeProfile.id
const activeProfileRouteId =
resolveRouteIdFromBaseUrl(activeProfile.baseUrl) ??
resolveProfileRoute(activeProfile.provider).routeId

if (!profileEnvApplied || activeProfileRouteId !== routeId) {
return routeOptions
}

const profileOptions = getProfileModelOptions(activeProfile)
if (profileOptions.length === 0) {
return routeOptions
}

const routeOptionsByValue = new Map(
routeOptions.flatMap(option => {
const value =
typeof option.value === 'string' ? option.value.trim().toLowerCase() : ''
return value ? [[value, option] as const] : []
}),
)
const merged: ModelOption[] = []
const seen = new Set<string>()

for (const option of profileOptions) {
const value = typeof option.value === 'string' ? option.value.trim() : ''
const key = value.toLowerCase()
if (!value || seen.has(key)) {
continue
}

seen.add(key)
merged.push(routeOptionsByValue.get(key) ?? option)
}

return merged
}

function getActiveRouteId(): string | null {
const activeProfile = getActiveProviderProfile()
return resolveActiveRouteIdFromEnv(process.env, {
Expand Down Expand Up @@ -185,15 +237,17 @@ async function loadDescriptorDiscoveryContext(
return null
}

const routeOptions = buildRouteCatalogModelOptions(
routeLabel,
staticEntries,
routeDefaultModel,
)

return {
kind: 'descriptor',
autoRefresh: false,
canRefresh,
optionsOverride: buildRouteCatalogModelOptions(
routeLabel,
staticEntries,
routeDefaultModel,
),
optionsOverride: mergeActiveProfileModelOptions(routeId, routeOptions),
routeId,
routeDefaultModel,
routeLabel,
Expand Down Expand Up @@ -230,16 +284,18 @@ async function loadDescriptorDiscoveryContext(
}
}

const routeOptions = buildRouteCatalogModelOptions(
routeLabel,
mergedEntries,
routeDefaultModel,
)

return {
kind: 'descriptor',
autoRefresh,
canRefresh,
discoveryState,
optionsOverride: buildRouteCatalogModelOptions(
routeLabel,
mergedEntries,
routeDefaultModel,
),
optionsOverride: mergeActiveProfileModelOptions(routeId, routeOptions),
routeId,
routeDefaultModel,
routeLabel,
Expand Down Expand Up @@ -461,10 +517,13 @@ function ModelPickerWrapper({
},
)

const nextOptions = buildRouteCatalogModelOptions(
discoveryContext.routeLabel,
result?.models ?? [],
discoveryContext.routeDefaultModel,
const nextOptions = mergeActiveProfileModelOptions(
discoveryContext.routeId,
buildRouteCatalogModelOptions(
discoveryContext.routeLabel,
result?.models ?? [],
discoveryContext.routeDefaultModel,
),
)
const changed = !haveSameModelOptions(optionsOverride ?? [], nextOptions)

Expand Down Expand Up @@ -734,10 +793,13 @@ async function refreshModelsAndSummarize(): Promise<string> {
...getOpenAIDiscoveryRequestOptions(discoveryContext.routeId),
forceRefresh: true,
})
const nextOptions = buildRouteCatalogModelOptions(
discoveryContext.routeLabel,
result?.models ?? [],
discoveryContext.routeDefaultModel,
const nextOptions = mergeActiveProfileModelOptions(
discoveryContext.routeId,
buildRouteCatalogModelOptions(
discoveryContext.routeLabel,
result?.models ?? [],
discoveryContext.routeDefaultModel,
),
)
const changed = !haveSameModelOptions(
discoveryContext.optionsOverride,
Expand Down