mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
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:
@@ -225,7 +225,7 @@ All providers except AWS Bedrock and OpenRouter support custom endpoints.
|
|||||||
|
|
||||||
### Server-Side Multi-Model Configuration
|
### Server-Side Multi-Model Configuration
|
||||||
|
|
||||||
Administrators can configure multiple server-side models that are available to all users without requiring personal API keys. Configure via `AI_MODELS_CONFIG` environment variable (JSON string) or `ai-models.json` file.
|
Administrators can configure multiple server-side models that are available to all users without requiring personal API keys. Configure via `AI_MODELS_CONFIG` environment variable (JSON string) or `ai-models.json` file. For a single-provider quick setup, list comma-separated model IDs in `AI_MODEL`.
|
||||||
|
|
||||||
### Admin Panel
|
### Admin Panel
|
||||||
|
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ npm run dev
|
|||||||
|
|
||||||
### 服务端多模型配置
|
### 服务端多模型配置
|
||||||
|
|
||||||
管理员可以配置多个服务端模型,让所有用户无需提供个人 API Key 即可使用。通过 `AI_MODELS_CONFIG` 环境变量(JSON 字符串)或 `ai-models.json` 文件配置。
|
管理员可以配置多个服务端模型,让所有用户无需提供个人 API Key 即可使用。通过 `AI_MODELS_CONFIG` 环境变量(JSON 字符串)或 `ai-models.json` 文件配置。如果只需要单 provider 下的多个模型,也可以直接在 `AI_MODEL` 中用逗号分隔模型 ID。
|
||||||
|
|
||||||
**模型要求**:此任务需要强大的模型能力,因为它涉及生成具有严格格式约束的长文本(draw.io XML)。推荐使用 Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro 和 DeepSeek V3.2/R1。
|
**模型要求**:此任务需要强大的模型能力,因为它涉及生成具有严格格式约束的长文本(draw.io XML)。推荐使用 Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro 和 DeepSeek V3.2/R1。
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,17 @@ AI_MODELS_CONFIG='{"providers":[{"name":"OpenAI","provider":"openai","models":["
|
|||||||
|
|
||||||
在项目根目录创建 `ai-models.json` 文件(或通过 `AI_MODELS_CONFIG_PATH` 指定路径)。
|
在项目根目录创建 `ai-models.json` 文件(或通过 `AI_MODELS_CONFIG_PATH` 指定路径)。
|
||||||
|
|
||||||
|
**方式三:`AI_MODEL` 用逗号分隔**(单 provider 的快速配置)
|
||||||
|
|
||||||
|
如果只需要暴露同一 provider 下的多个模型,可以直接在 `AI_MODEL` 里用逗号分隔。第一个模型会作为默认值。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_PROVIDER=doubao
|
||||||
|
AI_MODEL=doubao-seed-1-8-251215,doubao-seed-1-6-flash,doubao-seed-1-6-pro
|
||||||
|
```
|
||||||
|
|
||||||
|
这是等价 `ai-models.json` 的简写形式。如果需要配置多个 provider,或自定义 `apiKeyEnv` / `baseUrlEnv`,请使用方式一或方式二。
|
||||||
|
|
||||||
### 配置示例
|
### 配置示例
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -351,6 +351,17 @@ AI_MODELS_CONFIG='{"providers":[{"name":"OpenAI","provider":"openai","models":["
|
|||||||
|
|
||||||
Create an `ai-models.json` file in the project root (or set `AI_MODELS_CONFIG_PATH` to a custom location).
|
Create an `ai-models.json` file in the project root (or set `AI_MODELS_CONFIG_PATH` to a custom location).
|
||||||
|
|
||||||
|
**Option 3: Comma-separated `AI_MODEL`** (quick setup, single provider)
|
||||||
|
|
||||||
|
If you only need multiple models from one provider, list them in `AI_MODEL` separated by commas. The first model is treated as the default.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_PROVIDER=doubao
|
||||||
|
AI_MODEL=doubao-seed-1-8-251215,doubao-seed-1-6-flash,doubao-seed-1-6-pro
|
||||||
|
```
|
||||||
|
|
||||||
|
This is shorthand for the equivalent `ai-models.json`. For multiple providers or custom `apiKeyEnv` / `baseUrlEnv`, use Option 1 or 2 instead.
|
||||||
|
|
||||||
### Example Configuration
|
### Example Configuration
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ AWS BedrockとOpenRouter以外のすべてのプロバイダーはカスタム
|
|||||||
|
|
||||||
### サーバーサイドマルチモデル設定
|
### サーバーサイドマルチモデル設定
|
||||||
|
|
||||||
管理者は、ユーザーが個人のAPIキーを提供することなく利用できる複数のサーバーサイドモデルを設定できます。`AI_MODELS_CONFIG` 環境変数(JSON文字列)または `ai-models.json` ファイルで設定します。
|
管理者は、ユーザーが個人のAPIキーを提供することなく利用できる複数のサーバーサイドモデルを設定できます。`AI_MODELS_CONFIG` 環境変数(JSON文字列)または `ai-models.json` ファイルで設定します。同一プロバイダー内の複数モデルだけが必要な場合は、`AI_MODEL` にカンマ区切りでモデルIDを列挙する簡易設定も使えます。
|
||||||
|
|
||||||
**モデル要件**:このタスクは厳密なフォーマット制約(draw.io XML)を持つ長文テキスト生成を伴うため、強力なモデル機能が必要です。Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro、DeepSeek V3.2/R1を推奨します。
|
**モデル要件**:このタスクは厳密なフォーマット制約(draw.io XML)を持つ長文テキスト生成を伴うため、強力なモデル機能が必要です。Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro、DeepSeek V3.2/R1を推奨します。
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,17 @@ AI_MODELS_CONFIG='{"providers":[{"name":"OpenAI","provider":"openai","models":["
|
|||||||
|
|
||||||
プロジェクトルートに `ai-models.json` ファイルを作成します(または `AI_MODELS_CONFIG_PATH` でパスを指定)。
|
プロジェクトルートに `ai-models.json` ファイルを作成します(または `AI_MODELS_CONFIG_PATH` でパスを指定)。
|
||||||
|
|
||||||
|
**方法3:`AI_MODEL` をカンマ区切りで指定**(単一プロバイダーの簡易設定)
|
||||||
|
|
||||||
|
同一プロバイダー内の複数モデルだけを公開したい場合は、`AI_MODEL` にカンマ区切りで列挙できます。最初のモデルがデフォルトになります。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AI_PROVIDER=doubao
|
||||||
|
AI_MODEL=doubao-seed-1-8-251215,doubao-seed-1-6-flash,doubao-seed-1-6-pro
|
||||||
|
```
|
||||||
|
|
||||||
|
これは等価な `ai-models.json` の簡易表記です。複数のプロバイダーや、カスタム `apiKeyEnv` / `baseUrlEnv` を使う場合は、方法1または方法2を使ってください。
|
||||||
|
|
||||||
### 設定例
|
### 設定例
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
AI_PROVIDER=bedrock
|
AI_PROVIDER=bedrock
|
||||||
|
|
||||||
# AI_MODEL: The model ID for your chosen provider (REQUIRED)
|
# AI_MODEL: The model ID for your chosen provider (REQUIRED)
|
||||||
|
# Tip: For a single-provider quick multi-model setup, list comma-separated model IDs.
|
||||||
|
# The first one becomes the default and the rest appear in the model picker.
|
||||||
|
# For multiple providers or custom apiKeyEnv/baseUrlEnv, use AI_MODELS_CONFIG / ai-models.json instead.
|
||||||
|
# Example: AI_MODEL=doubao-seed-1-8-251215,doubao-seed-1-6-flash,doubao-seed-1-6-pro
|
||||||
AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||||
|
|
||||||
# AWS Bedrock Configuration
|
# AWS Bedrock Configuration
|
||||||
|
|||||||
@@ -729,8 +729,10 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
(overrides?.provider === "vertexai" && overrides?.vertexApiKey))
|
(overrides?.provider === "vertexai" && overrides?.vertexApiKey))
|
||||||
)
|
)
|
||||||
|
|
||||||
// Use client override if provided, otherwise fall back to env vars
|
// Use client override if provided, otherwise fall back to env vars.
|
||||||
const modelId = overrides?.modelId || process.env.AI_MODEL
|
// 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 (!modelId) {
|
||||||
if (isClientOverride) {
|
if (isClientOverride) {
|
||||||
@@ -1490,7 +1492,9 @@ export function supportsImageInput(modelId: string): boolean {
|
|||||||
* Throws if the model doesn't support image input.
|
* Throws if the model doesn't support image input.
|
||||||
*/
|
*/
|
||||||
export function getValidationModel(): ReturnType<typeof getAIModel>["model"] {
|
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) {
|
if (!modelId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -62,6 +62,53 @@ function getConfigPath(): string {
|
|||||||
return path.join(process.cwd(), "ai-models.json")
|
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> {
|
export async function loadEnvServerModelsConfig(): Promise<ServerModelsConfig | null> {
|
||||||
// Priority 1: AI_MODELS_CONFIG env var (JSON string) - for cloud deployments
|
// Priority 1: AI_MODELS_CONFIG env var (JSON string) - for cloud deployments
|
||||||
const envConfig = process.env.AI_MODELS_CONFIG
|
const envConfig = process.env.AI_MODELS_CONFIG
|
||||||
@@ -85,9 +132,7 @@ export async function loadEnvServerModelsConfig(): Promise<ServerModelsConfig |
|
|||||||
const json = JSON.parse(jsonStr)
|
const json = JSON.parse(jsonStr)
|
||||||
return ServerModelsConfigSchema.parse(json)
|
return ServerModelsConfigSchema.parse(json)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err?.code === "ENOENT") {
|
if (err?.code !== "ENOENT") {
|
||||||
return null
|
|
||||||
}
|
|
||||||
console.error(
|
console.error(
|
||||||
"[server-model-config] Failed to load ai-models.json:",
|
"[server-model-config] Failed to load ai-models.json:",
|
||||||
err,
|
err,
|
||||||
@@ -96,6 +141,10 @@ export async function loadEnvServerModelsConfig(): Promise<ServerModelsConfig |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Priority 3: AI_MODEL with comma-separated values + AI_PROVIDER
|
||||||
|
return configFromCommaSeparatedAiModel()
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadRawServerModelsConfig(): Promise<ServerModelsConfig | null> {
|
export async function loadRawServerModelsConfig(): Promise<ServerModelsConfig | null> {
|
||||||
const envConfig = await loadEnvServerModelsConfig()
|
const envConfig = await loadEnvServerModelsConfig()
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,44 @@ describe("loadFlattenedServerModels", () => {
|
|||||||
expect(defaultModel.modelId).toBe("gpt-4o") // First model of default provider
|
expect(defaultModel.modelId).toBe("gpt-4o") // First model of default provider
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("falls back to comma-separated AI_MODEL when no other config is set", async () => {
|
||||||
|
process.env.AI_MODELS_CONFIG = ""
|
||||||
|
process.env.AI_MODELS_CONFIG_PATH = `non-existent-config-${Date.now()}.json`
|
||||||
|
process.env.AI_PROVIDER = "openai"
|
||||||
|
process.env.AI_MODEL = "gpt-4o, gpt-4o-mini, gpt-4o"
|
||||||
|
|
||||||
|
const models = await loadFlattenedServerModels()
|
||||||
|
|
||||||
|
// Trims, deduplicates, and preserves order
|
||||||
|
expect(models.map((m) => m.modelId)).toEqual(["gpt-4o", "gpt-4o-mini"])
|
||||||
|
expect(models.every((m) => m.provider === "openai")).toBe(true)
|
||||||
|
|
||||||
|
// First model is marked default (provider has default: true)
|
||||||
|
const defaults = models.filter((m) => m.isDefault)
|
||||||
|
expect(defaults.length).toBe(1)
|
||||||
|
expect(defaults[0].modelId).toBe("gpt-4o")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not synthesize when AI_MODEL has no comma", async () => {
|
||||||
|
process.env.AI_MODELS_CONFIG = ""
|
||||||
|
process.env.AI_MODELS_CONFIG_PATH = `non-existent-config-${Date.now()}.json`
|
||||||
|
process.env.AI_PROVIDER = "openai"
|
||||||
|
process.env.AI_MODEL = "gpt-4o"
|
||||||
|
|
||||||
|
const models = await loadFlattenedServerModels()
|
||||||
|
expect(models).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not synthesize when AI_PROVIDER is unset", async () => {
|
||||||
|
process.env.AI_MODELS_CONFIG = ""
|
||||||
|
process.env.AI_MODELS_CONFIG_PATH = `non-existent-config-${Date.now()}.json`
|
||||||
|
delete process.env.AI_PROVIDER
|
||||||
|
process.env.AI_MODEL = "gpt-4o, gpt-4o-mini"
|
||||||
|
|
||||||
|
const models = await loadFlattenedServerModels()
|
||||||
|
expect(models).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
it("preserves apiKeyEnv array in flattened models for load balancing", async () => {
|
it("preserves apiKeyEnv array in flattened models for load balancing", async () => {
|
||||||
const config: ServerModelsConfig = {
|
const config: ServerModelsConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
Reference in New Issue
Block a user