Feat/multi model provider support (#692)
* test: add tests for provider model env updates and multi-model profiles Add comprehensive tests covering: - OPENAI_MODEL/ANTHROPIC_MODEL env updates on provider activation - Cross-provider type switches (openai ↔ anthropic) clearing stale env - Multi-model profile activation using only the first model for env vars - Model options cache population from comma-separated model lists - getProfileModelOptions generating correct ModelOption arrays * feat: multi-model provider support and model auto-switch Support comma-separated model names in provider profiles (e.g. "glm-4.7, glm-4.7-flash"). The first model is used as default on activation; all models appear in the /model picker for easy switching. When switching active providers, the session model now automatically updates to the new provider's first model. The multi-model list is preserved across switches and /model selections. Changes: - Add parseModelList, getPrimaryModel, hasMultipleModels utilities with full test coverage (19 tests) - Use getPrimaryModel when applying profiles to process.env so only the primary model is set in OPENAI_MODEL/ANTHROPIC_MODEL - Update ProviderManager UI to hint at multi-model syntax and show model count in provider list summaries - Populate model options cache from multi-model profiles on activation so all models appear in /model picker regardless of base URL type - Guard persistActiveProviderProfileModel against overwriting comma-separated lists: models already in the profile are session selections, not profile edits - Set AppState.mainLoopModel to the actual model string on provider switch so Anthropic profiles use the configured model instead of falling back to the built-in default * fix: only show profile models when provider profile env is applied Guard the profile model picker options behind a PROFILE_ENV_APPLIED check. getActiveProviderProfile() has a ?? profiles[0] fallback that returns the first profile even when no profile is explicitly active, causing users with inactive profiles to lose all standard model options (Opus, Haiku, etc.) from the /model picker. * fix: show all model names for profiles with 3 or fewer models Instead of a summary format for multi-model profiles, display all model names when there are 3 or fewer. Only use the "+ N more" format for profiles with 4+ models. * fix: preserve standard model options in picker alongside profile models The previous implementation used an early return that replaced all standard picker options (Opus, Haiku, Sonnet for Anthropic; Codex/GPT models for OpenAI) with only the profile's custom models. Changes: - Collect profile models into a shared array instead of early returning - Append profile models to firstParty path (Opus + Haiku + Sonnet + custom) - Append profile models to PAYG 3P path (Codex + Sonnet + Opus + Haiku + custom) - Guard collection behind PROFILE_ENV_APPLIED to avoid ?? profiles[0] fallback Fixes review feedback: standard models are no longer hidden when a provider profile with custom models is active. Users see both the standard options and their profile's models. --------- Co-authored-by: Ali Alakbarli <ali.alakbarli@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
type ProviderProfile,
|
||||
} from './config.js'
|
||||
import type { ModelOption } from './model/modelOptions.js'
|
||||
import { getPrimaryModel, parseModelList } from './providerModels.js'
|
||||
|
||||
export type ProviderPreset =
|
||||
| 'anthropic'
|
||||
@@ -331,7 +332,7 @@ function isProcessEnvAlignedWithProfile(
|
||||
return (
|
||||
!hasProviderSelectionFlags(processEnv) &&
|
||||
sameOptionalEnvValue(processEnv.ANTHROPIC_BASE_URL, profile.baseUrl) &&
|
||||
sameOptionalEnvValue(processEnv.ANTHROPIC_MODEL, profile.model) &&
|
||||
sameOptionalEnvValue(processEnv.ANTHROPIC_MODEL, getPrimaryModel(profile.model)) &&
|
||||
(!includeApiKey ||
|
||||
sameOptionalEnvValue(processEnv.ANTHROPIC_API_KEY, profile.apiKey))
|
||||
)
|
||||
@@ -346,7 +347,7 @@ function isProcessEnvAlignedWithProfile(
|
||||
processEnv.CLAUDE_CODE_USE_VERTEX === undefined &&
|
||||
processEnv.CLAUDE_CODE_USE_FOUNDRY === undefined &&
|
||||
sameOptionalEnvValue(processEnv.OPENAI_BASE_URL, profile.baseUrl) &&
|
||||
sameOptionalEnvValue(processEnv.OPENAI_MODEL, profile.model) &&
|
||||
sameOptionalEnvValue(processEnv.OPENAI_MODEL, getPrimaryModel(profile.model)) &&
|
||||
(!includeApiKey ||
|
||||
sameOptionalEnvValue(processEnv.OPENAI_API_KEY, profile.apiKey))
|
||||
)
|
||||
@@ -397,7 +398,7 @@ export function applyProviderProfileToProcessEnv(profile: ProviderProfile): void
|
||||
process.env[PROFILE_ENV_APPLIED_FLAG] = '1'
|
||||
process.env[PROFILE_ENV_APPLIED_ID] = profile.id
|
||||
|
||||
process.env.ANTHROPIC_MODEL = profile.model
|
||||
process.env.ANTHROPIC_MODEL = getPrimaryModel(profile.model)
|
||||
if (profile.provider === 'anthropic') {
|
||||
process.env.ANTHROPIC_BASE_URL = profile.baseUrl
|
||||
|
||||
@@ -416,7 +417,7 @@ export function applyProviderProfileToProcessEnv(profile: ProviderProfile): void
|
||||
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = profile.baseUrl
|
||||
process.env.OPENAI_MODEL = profile.model
|
||||
process.env.OPENAI_MODEL = getPrimaryModel(profile.model)
|
||||
|
||||
if (profile.apiKey) {
|
||||
process.env.OPENAI_API_KEY = profile.apiKey
|
||||
@@ -581,6 +582,16 @@ export function persistActiveProviderProfileModel(
|
||||
return null
|
||||
}
|
||||
|
||||
// If the model is already part of the profile's model list, don't
|
||||
// overwrite the field. This preserves comma-separated model lists like
|
||||
// "glm-4.5, glm-4.7". Switching between models in the list is a
|
||||
// session-level choice handled by mainLoopModelOverride, not a profile
|
||||
// edit — the profile's model list should only change via explicit edit.
|
||||
const existingModels = parseModelList(activeProfile.model)
|
||||
if (existingModels.includes(nextModel)) {
|
||||
return activeProfile
|
||||
}
|
||||
|
||||
saveGlobalConfig(current => {
|
||||
const currentProfiles = getProviderProfiles(current)
|
||||
const profileIndex = currentProfiles.findIndex(
|
||||
@@ -623,6 +634,23 @@ export function persistActiveProviderProfileModel(
|
||||
return resolvedProfile
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate model options from a provider profile's model field.
|
||||
* Each comma-separated model becomes a separate option in the picker.
|
||||
*/
|
||||
export function getProfileModelOptions(profile: ProviderProfile): ModelOption[] {
|
||||
const models = parseModelList(profile.model)
|
||||
if (models.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return models.map(model => ({
|
||||
value: model,
|
||||
label: model,
|
||||
description: `Provider: ${profile.name}`,
|
||||
}))
|
||||
}
|
||||
|
||||
export function setActiveProviderProfile(
|
||||
profileId: string,
|
||||
): ProviderProfile | null {
|
||||
@@ -634,10 +662,20 @@ export function setActiveProviderProfile(
|
||||
return null
|
||||
}
|
||||
|
||||
const profileModelOptions = getProfileModelOptions(activeProfile)
|
||||
|
||||
saveGlobalConfig(config => ({
|
||||
...config,
|
||||
activeProviderProfileId: profileId,
|
||||
openaiAdditionalModelOptionsCache: getModelCacheByProfile(profileId, config),
|
||||
openaiAdditionalModelOptionsCache: profileModelOptions.length > 0
|
||||
? profileModelOptions
|
||||
: getModelCacheByProfile(profileId, config),
|
||||
openaiAdditionalModelOptionsCacheByProfile: {
|
||||
...(config.openaiAdditionalModelOptionsCacheByProfile ?? {}),
|
||||
[profileId]: profileModelOptions.length > 0
|
||||
? profileModelOptions
|
||||
: (config.openaiAdditionalModelOptionsCacheByProfile?.[profileId] ?? []),
|
||||
},
|
||||
}))
|
||||
|
||||
applyProviderProfileToProcessEnv(activeProfile)
|
||||
|
||||
Reference in New Issue
Block a user