feat: support comma-separated AI_MODEL for quick multi-model setup (#870)

Users expected setting AI_MODEL to a comma-separated list to expose
multiple models in the picker, but the value was used verbatim as a
single model id, leaving the picker with only the "Server Default"
fallback.

Add a third-priority fallback in loadEnvServerModelsConfig: when
AI_MODELS_CONFIG and ai-models.json are both absent, AI_MODEL contains
a comma, and AI_PROVIDER is set to a known provider, synthesize an
equivalent ServerModelsConfig with the provider's models trimmed,
deduplicated, and the first marked as default.

Also makes getAIModel and getValidationModel pick the first comma-split
value when falling back to AI_MODEL, so requests started before the
client picker hydrates still resolve to a real model id.

Docs (en/cn/ja) and env.example updated; tests cover the new fallback
plus the no-comma / no-AI_PROVIDER negative cases.
This commit is contained in:
Dayuan Jiang
2026-06-15 14:27:55 +09:00
committed by GitHub
parent 0f9699843f
commit 8e42dd9da8
10 changed files with 140 additions and 12 deletions

View File

@@ -729,8 +729,10 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
(overrides?.provider === "vertexai" && overrides?.vertexApiKey))
)
// Use client override if provided, otherwise fall back to env vars
const modelId = overrides?.modelId || process.env.AI_MODEL
// Use client override if provided, otherwise fall back to env vars.
// AI_MODEL may be comma-separated (multi-model fallback); pick the first.
const envModel = process.env.AI_MODEL?.split(",")[0]?.trim() || undefined
const modelId = overrides?.modelId || envModel
if (!modelId) {
if (isClientOverride) {
@@ -1490,7 +1492,9 @@ export function supportsImageInput(modelId: string): boolean {
* Throws if the model doesn't support image input.
*/
export function getValidationModel(): ReturnType<typeof getAIModel>["model"] {
const modelId = process.env.VALIDATION_MODEL || process.env.AI_MODEL
// AI_MODEL may be comma-separated (multi-model fallback); pick the first.
const envFallback = process.env.AI_MODEL?.split(",")[0]?.trim() || undefined
const modelId = process.env.VALIDATION_MODEL || envFallback
if (!modelId) {
throw new Error(

View File

@@ -62,6 +62,53 @@ function getConfigPath(): string {
return path.join(process.cwd(), "ai-models.json")
}
/**
* Synthesize a config from a comma-separated AI_MODEL value (Priority 3 fallback).
* Lets users expose multiple models without authoring AI_MODELS_CONFIG / ai-models.json.
* Triggers only when AI_MODEL contains a comma AND AI_PROVIDER is set to a known provider.
*/
function configFromCommaSeparatedAiModel(): ServerModelsConfig | null {
const aiModel = process.env.AI_MODEL
if (!aiModel || !aiModel.includes(",")) return null
const aiProvider = process.env.AI_PROVIDER
if (!aiProvider) {
console.warn(
"[server-model-config] AI_MODEL contains commas but AI_PROVIDER is not set; " +
"skipping multi-model fallback. Set AI_PROVIDER, or use AI_MODELS_CONFIG / ai-models.json.",
)
return null
}
if (!(aiProvider in PROVIDER_INFO)) {
console.warn(
`[server-model-config] AI_PROVIDER="${aiProvider}" is not a known provider; skipping multi-model fallback.`,
)
return null
}
const models = Array.from(
new Set(
aiModel
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0),
),
)
if (models.length === 0) return null
const providerName = aiProvider as ProviderName
return {
providers: [
{
name: PROVIDER_INFO[providerName]?.label || providerName,
provider: providerName,
models,
default: true,
},
],
}
}
export async function loadEnvServerModelsConfig(): Promise<ServerModelsConfig | null> {
// Priority 1: AI_MODELS_CONFIG env var (JSON string) - for cloud deployments
const envConfig = process.env.AI_MODELS_CONFIG
@@ -85,15 +132,17 @@ export async function loadEnvServerModelsConfig(): Promise<ServerModelsConfig |
const json = JSON.parse(jsonStr)
return ServerModelsConfigSchema.parse(json)
} catch (err: any) {
if (err?.code === "ENOENT") {
if (err?.code !== "ENOENT") {
console.error(
"[server-model-config] Failed to load ai-models.json:",
err,
)
return null
}
console.error(
"[server-model-config] Failed to load ai-models.json:",
err,
)
return null
}
// Priority 3: AI_MODEL with comma-separated values + AI_PROVIDER
return configFromCommaSeparatedAiModel()
}
export async function loadRawServerModelsConfig(): Promise<ServerModelsConfig | null> {