mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-09 11:17:04 +08:00
fix: disambiguate colliding server model IDs
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { z } from "zod"
|
||||
@@ -56,6 +57,36 @@ function slugify(name: string): string {
|
||||
.replace(/^-|-$/g, "")
|
||||
}
|
||||
|
||||
function getProviderSlugs(providers: ServerProviderConfig[]): string[] {
|
||||
const baseSlugs = providers.map((provider) => slugify(provider.name))
|
||||
const slugCounts = new Map<string, number>()
|
||||
|
||||
for (const slug of baseSlugs) {
|
||||
slugCounts.set(slug, (slugCounts.get(slug) ?? 0) + 1)
|
||||
}
|
||||
|
||||
return providers.map((provider, index) => {
|
||||
const baseSlug = baseSlugs[index]
|
||||
if (baseSlug && slugCounts.get(baseSlug) === 1) return baseSlug
|
||||
|
||||
// Preserve existing IDs unless a slug is empty or ambiguous. The
|
||||
// suffix keeps colliding providers stable across config reloads and
|
||||
// includes credential routing fields so identical display names can
|
||||
// still resolve to the intended server configuration.
|
||||
const identity = JSON.stringify([
|
||||
provider.name,
|
||||
provider.provider,
|
||||
provider.apiKeyEnv ?? null,
|
||||
provider.baseUrlEnv ?? null,
|
||||
])
|
||||
const suffix = createHash("sha256")
|
||||
.update(identity)
|
||||
.digest("hex")
|
||||
.slice(0, 12)
|
||||
return `${baseSlug || provider.provider}-${suffix}`
|
||||
})
|
||||
}
|
||||
|
||||
function getConfigPath(): string {
|
||||
const custom = process.env.AI_MODELS_CONFIG_PATH
|
||||
if (custom && custom.trim().length > 0) return custom
|
||||
@@ -189,13 +220,14 @@ export async function loadFlattenedServerModels(): Promise<
|
||||
const defaultModelId = process.env.AI_MODEL
|
||||
|
||||
const flattened: FlattenedServerModel[] = []
|
||||
const providerSlugs = getProviderSlugs(cfg.providers)
|
||||
|
||||
for (const p of cfg.providers) {
|
||||
for (const [providerIndex, p] of cfg.providers.entries()) {
|
||||
const providerLabel =
|
||||
p.name || PROVIDER_INFO[p.provider]?.label || p.provider
|
||||
|
||||
// Use slugified name for unique ID (supports multiple API keys per provider)
|
||||
const nameSlug = slugify(p.name)
|
||||
const nameSlug = providerSlugs[providerIndex]
|
||||
|
||||
for (const modelId of p.models) {
|
||||
const id = `server:${nameSlug}:${modelId}`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
||||
import { _resetForTests } from "@/lib/admin/settings"
|
||||
import {
|
||||
findServerModelById,
|
||||
loadFlattenedServerModels,
|
||||
type ServerModelsConfig,
|
||||
ServerModelsConfigSchema,
|
||||
@@ -232,4 +233,74 @@ describe("loadFlattenedServerModels", () => {
|
||||
expect(models.length).toBe(1)
|
||||
expect(models[0].apiKeyEnv).toEqual(["OPENAI_KEY_1", "OPENAI_KEY_2"])
|
||||
})
|
||||
|
||||
it("keeps colliding provider name slugs routed to the right credentials", async () => {
|
||||
const config: ServerModelsConfig = {
|
||||
providers: [
|
||||
{
|
||||
name: "Open AI",
|
||||
provider: "openai",
|
||||
models: ["shared-model"],
|
||||
apiKeyEnv: "OPENAI_KEY_PRIMARY",
|
||||
},
|
||||
{
|
||||
name: "Open-AI",
|
||||
provider: "openai",
|
||||
models: ["shared-model"],
|
||||
apiKeyEnv: "OPENAI_KEY_BACKUP",
|
||||
},
|
||||
],
|
||||
}
|
||||
process.env.AI_MODELS_CONFIG = JSON.stringify(config)
|
||||
|
||||
const models = await loadFlattenedServerModels()
|
||||
|
||||
expect(new Set(models.map((model) => model.id)).size).toBe(2)
|
||||
const backup = await findServerModelById(models[1].id)
|
||||
expect(backup?.apiKeyEnv).toBe("OPENAI_KEY_BACKUP")
|
||||
})
|
||||
|
||||
it("creates distinct IDs for non-Latin provider names", async () => {
|
||||
const config: ServerModelsConfig = {
|
||||
providers: [
|
||||
{
|
||||
name: "生产环境",
|
||||
provider: "openai",
|
||||
models: ["shared-model"],
|
||||
apiKeyEnv: "OPENAI_KEY_PRIMARY",
|
||||
},
|
||||
{
|
||||
name: "备用环境",
|
||||
provider: "openai",
|
||||
models: ["shared-model"],
|
||||
apiKeyEnv: "OPENAI_KEY_BACKUP",
|
||||
},
|
||||
],
|
||||
}
|
||||
process.env.AI_MODELS_CONFIG = JSON.stringify(config)
|
||||
|
||||
const models = await loadFlattenedServerModels()
|
||||
|
||||
expect(new Set(models.map((model) => model.id)).size).toBe(2)
|
||||
expect(models.every((model) => !model.id.startsWith("server::"))).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it("preserves existing IDs when provider slugs do not collide", async () => {
|
||||
const config: ServerModelsConfig = {
|
||||
providers: [
|
||||
{
|
||||
name: "OpenAI Production",
|
||||
provider: "openai",
|
||||
models: ["gpt-4o"],
|
||||
},
|
||||
],
|
||||
}
|
||||
process.env.AI_MODELS_CONFIG = JSON.stringify(config)
|
||||
|
||||
const models = await loadFlattenedServerModels()
|
||||
|
||||
expect(models[0].id).toBe("server:openai-production:gpt-4o")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user