diff --git a/.gitignore b/.gitignore index 7427ecb..0e8010a 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,6 @@ ai-models.json # local backups *.bak .gstack/ + +# admin panel settings (contains secrets) +data/ diff --git a/app/[lang]/admin/page.tsx b/app/[lang]/admin/page.tsx index 6045be1..58c97ce 100644 --- a/app/[lang]/admin/page.tsx +++ b/app/[lang]/admin/page.tsx @@ -90,6 +90,14 @@ interface AdminProvider { isDefault?: boolean } +// Provider defined in AI_MODELS_CONFIG / ai-models.json — shown read-only +interface EnvProvider { + name: string + provider: ProviderName + models: string[] + isDefault: boolean +} + async function adminFetch(path: string, pw: string, init?: RequestInit) { const res = await fetch(getApiEndpoint(path), { ...init, @@ -726,11 +734,13 @@ function ProviderDetail({ function ModelsSection({ providers, + envProviders, disabled, password, onChange, }: { providers: AdminProvider[] + envProviders: EnvProvider[] disabled: boolean password: string onChange: (providers: AdminProvider[]) => void @@ -739,6 +749,7 @@ function ModelsSection({ providers[0]?.id ?? null, ) const selected = providers.find((p) => p.id === selectedId) + const selectedEnv = envProviders.find((p) => `env:${p.name}` === selectedId) const addProvider = (provider: ProviderName) => { const newProvider: AdminProvider = { @@ -774,12 +785,38 @@ function ModelsSection({ {/* Provider list */}
Add a provider to offer server-side models to all users.
+ Defined in AI_MODELS_CONFIG / ai-models.json + — read-only here. Edit the environment + configuration to change it. +
Select or add a provider to configure its credentials @@ -870,6 +948,7 @@ export default function AdminPage() { // Models section state const [providers, setProviders] = useState([]) + const [envProviders, setEnvProviders] = useState([]) const [savedProviders, setSavedProviders] = useState("[]") const providersDirty = JSON.stringify(providers) !== savedProviders @@ -912,9 +991,13 @@ export default function AdminPage() { ) const applyProvidersResponse = useCallback( - (data: { providers: AdminProvider[] }) => { + (data: { + providers: AdminProvider[] + envProviders?: EnvProvider[] + }) => { setProviders(data.providers) setSavedProviders(JSON.stringify(data.providers)) + setEnvProviders(data.envProviders ?? []) }, [], ) @@ -1214,6 +1297,7 @@ export default function AdminPage() { { diff --git a/app/api/admin/providers/route.ts b/app/api/admin/providers/route.ts index d93e5cc..e2ec7f0 100644 --- a/app/api/admin/providers/route.ts +++ b/app/api/admin/providers/route.ts @@ -8,21 +8,32 @@ import { validateAdminProviders, } from "@/lib/admin/providers" import { isSettingsWritable, saveSettings } from "@/lib/admin/settings" +import { loadEnvServerModelsConfig } from "@/lib/server-model-config" export const runtime = "nodejs" export const dynamic = "force-dynamic" -function payload() { +async function payload() { + // Env-based providers (AI_MODELS_CONFIG / ai-models.json) are shown + // read-only in the panel; their credentials live in the environment + const envConfig = await loadEnvServerModelsConfig() return { writable: isSettingsWritable(), providers: maskAdminProviders(loadAdminProviders()), + envProviders: + envConfig?.providers.map((p) => ({ + name: p.name, + provider: p.provider, + models: p.models, + isDefault: !!p.default, + })) ?? [], } } export async function GET(req: Request) { const authError = checkAdminAuth(req) if (authError) return authError - return Response.json(payload()) + return Response.json(await payload()) } export async function PUT(req: Request) { @@ -60,12 +71,14 @@ export async function PUT(req: Request) { const stored = loadAdminProviders() const merged = mergeSecrets(parsed.data, stored) - const validationError = validateAdminProviders(merged) + const envConfig = await loadEnvServerModelsConfig() + const envNames = envConfig?.providers.map((p) => p.name) ?? [] + const validationError = validateAdminProviders(merged, envNames) if (validationError) { return Response.json({ error: validationError }, { status: 400 }) } saveSettings(deriveEnvUpdates(merged, stored)) - return Response.json(payload()) + return Response.json(await payload()) } diff --git a/lib/admin/providers.ts b/lib/admin/providers.ts index da3dc95..ca0f564 100644 --- a/lib/admin/providers.ts +++ b/lib/admin/providers.ts @@ -8,9 +8,14 @@ import { type MaskedSecret, maskSecret } from "./auth" import { loadSettings } from "./settings" // Admin-configured providers, mirroring the user ModelConfigDialog's data -// model but stored server-side. The list itself is stored in settings.json -// under ADMIN_PROVIDERS; on save we derive AI_MODELS_CONFIG plus the -// credential env vars so the existing runtime picks everything up unchanged. +// model but stored server-side (settings.json, ADMIN_PROVIDERS key). +// +// They COEXIST with an env-based AI_MODELS_CONFIG / ai-models.json: +// loadRawServerModelsConfig() merges the env baseline with the panel's +// providers at read time, so .env stays authoritative for its own entries. +// Panel credentials are written to ADMIN_-prefixed env vars (wired up via +// apiKeyEnv/baseUrlEnv) so they never shadow standard vars like +// OPENAI_API_KEY that env-based entries may rely on. export const ADMIN_PROVIDERS_KEY = "ADMIN_PROVIDERS" @@ -60,30 +65,26 @@ const SECRET_FIELDS = [ "vertexApiKey", ] as const -// Default credential env vars per provider (first instance). Later -// instances of the same provider get a _2/_3 suffix wired up via -// apiKeyEnv/baseUrlEnv in the generated AI_MODELS_CONFIG. -// bedrock/vertexai use fixed env vars the runtime reads directly, -// so only one instance of each can carry server credentials. -const CRED_ENV: Partial> = { - openai: { key: "OPENAI_API_KEY", url: "OPENAI_BASE_URL" }, - anthropic: { key: "ANTHROPIC_API_KEY", url: "ANTHROPIC_BASE_URL" }, - google: { key: "GOOGLE_GENERATIVE_AI_API_KEY", url: "GOOGLE_BASE_URL" }, - azure: { key: "AZURE_API_KEY", url: "AZURE_BASE_URL" }, - ollama: { key: "OLLAMA_API_KEY", url: "OLLAMA_BASE_URL" }, - openrouter: { key: "OPENROUTER_API_KEY", url: "OPENROUTER_BASE_URL" }, - deepseek: { key: "DEEPSEEK_API_KEY", url: "DEEPSEEK_BASE_URL" }, - siliconflow: { key: "SILICONFLOW_API_KEY", url: "SILICONFLOW_BASE_URL" }, - sglang: { key: "SGLANG_API_KEY", url: "SGLANG_BASE_URL" }, - gateway: { key: "AI_GATEWAY_API_KEY", url: "AI_GATEWAY_BASE_URL" }, - doubao: { key: "DOUBAO_API_KEY", url: "DOUBAO_BASE_URL" }, - modelscope: { key: "MODELSCOPE_API_KEY", url: "MODELSCOPE_BASE_URL" }, - glm: { key: "GLM_API_KEY", url: "GLM_BASE_URL" }, - qwen: { key: "QWEN_API_KEY", url: "QWEN_BASE_URL" }, - kimi: { key: "KIMI_API_KEY", url: "KIMI_BASE_URL" }, - qiniu: { key: "QINIU_API_KEY", url: "QINIU_BASE_URL" }, - minimax: { key: "MINIMAX_API_KEY", url: "MINIMAX_BASE_URL" }, - novita: { key: "NOVITA_API_KEY", url: "NOVITA_BASE_URL" }, +// Providers whose credentials the runtime reads from fixed env vars +// (no apiKeyEnv support), so the panel writes those vars directly and +// only one instance of each is allowed. edgeone needs no credentials. +const FIXED_CRED_PROVIDERS: ProviderName[] = ["bedrock", "vertexai", "ollama"] + +// ADMIN_-prefixed env var names for instance `index` (0-based) of a provider +function credEnvNames( + provider: ProviderName, + index: number, +): { key?: string; url?: string } { + if (FIXED_CRED_PROVIDERS.includes(provider) || provider === "edgeone") { + return {} + } + const prefix = + provider === "gateway" ? "AI_GATEWAY" : provider.toUpperCase() + const suffix = index === 0 ? "" : `_${index + 1}` + return { + key: `ADMIN_${prefix}_API_KEY${suffix}`, + url: `ADMIN_${prefix}_BASE_URL${suffix}`, + } } export function loadAdminProviders(): StoredAdminProvider[] { @@ -144,10 +145,15 @@ export function mergeSecrets( }) } +function displayName(p: StoredAdminProvider): string { + return p.name?.trim() || PROVIDER_INFO[p.provider].label +} + export function validateAdminProviders( list: StoredAdminProvider[], + envProviderNames: string[] = [], ): string | null { - for (const single of ["bedrock", "vertexai"] as const) { + for (const single of FIXED_CRED_PROVIDERS) { if (list.filter((p) => p.provider === single).length > 1) { return `Only one ${PROVIDER_INFO[single].label} provider is supported (its credentials use fixed environment variables).` } @@ -156,29 +162,43 @@ export function validateAdminProviders( if (new Set(names).size !== names.length) { return "Provider display names must be unique." } + const envNames = new Set(envProviderNames) + const clash = names.find((n) => envNames.has(n)) + if (clash) { + return `"${clash}" is already defined in AI_MODELS_CONFIG / ai-models.json. Use a different display name.` + } if (list.filter((p) => p.isDefault).length > 1) { return "Only one provider can be the default." } return null } -function displayName(p: StoredAdminProvider): string { - return p.name?.trim() || PROVIDER_INFO[p.provider].label +// The panel's contribution to the server models config, derived at read +// time and merged with the env baseline by loadRawServerModelsConfig(). +export function adminProvidersToConfig( + list: StoredAdminProvider[], +): ServerModelsConfig { + const config: ServerModelsConfig = { providers: [] } + const indexByProvider = new Map() + for (const p of list) { + const index = indexByProvider.get(p.provider) ?? 0 + indexByProvider.set(p.provider, index + 1) + if (p.models.length === 0) continue + const env = credEnvNames(p.provider, index) + config.providers.push({ + name: displayName(p), + provider: p.provider, + models: p.models, + ...(env.key && p.apiKey ? { apiKeyEnv: env.key } : {}), + ...(env.url && p.baseUrl ? { baseUrlEnv: env.url } : {}), + ...(p.isDefault ? { default: true } : {}), + }) + } + return config } -// Env var names for instance `index` (0-based) of a provider -function credEnvNames( - provider: ProviderName, - index: number, -): { key?: string; url?: string } { - const base = CRED_ENV[provider] - if (!base) return {} - if (index === 0) return base - return { key: `${base.key}_${index + 1}`, url: `${base.url}_${index + 1}` } -} - -// All env updates derived from the provider list: credential vars, -// AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL for the default model. +// Settings updates derived from the provider list: credential env vars, +// the stored list itself, and AI_PROVIDER/AI_MODEL when a default is set. // Keys derived from `previous` but absent now are set to null (removed, // falling back to the environment). export function deriveEnvUpdates( @@ -190,9 +210,7 @@ export function deriveEnvUpdates( // Clear everything the previous list owned, then overwrite below for (const key of derivedEnvKeys(previous)) updates[key] = null - const config: ServerModelsConfig = { providers: [] } const indexByProvider = new Map() - for (const p of list) { const index = indexByProvider.get(p.provider) ?? 0 indexByProvider.set(p.provider, index + 1) @@ -205,51 +223,32 @@ export function deriveEnvUpdates( } else if (p.provider === "vertexai") { if (p.vertexApiKey) updates.GOOGLE_VERTEX_API_KEY = p.vertexApiKey if (p.baseUrl) updates.GOOGLE_VERTEX_BASE_URL = p.baseUrl - } - - const env = credEnvNames(p.provider, index) - if (env.key && p.apiKey) updates[env.key] = p.apiKey - if (env.url && p.baseUrl) updates[env.url] = p.baseUrl - - if (p.models.length > 0) { - config.providers.push({ - name: displayName(p), - provider: p.provider, - models: p.models, - // First instances use the default env vars the runtime - // already falls back to; only extras need explicit wiring - ...(index > 0 && env.key && p.apiKey - ? { apiKeyEnv: env.key } - : {}), - ...(index > 0 && env.url && p.baseUrl - ? { baseUrlEnv: env.url } - : {}), - ...(p.isDefault ? { default: true } : {}), - }) + } else if (p.provider === "ollama") { + if (p.apiKey) updates.OLLAMA_API_KEY = p.apiKey + if (p.baseUrl) updates.OLLAMA_BASE_URL = p.baseUrl + } else { + const env = credEnvNames(p.provider, index) + if (env.key && p.apiKey) updates[env.key] = p.apiKey + if (env.url && p.baseUrl) updates[env.url] = p.baseUrl } } - if (config.providers.length > 0) { - updates[ADMIN_PROVIDERS_KEY] = JSON.stringify(list) - updates.AI_MODELS_CONFIG = JSON.stringify(config) - const defaultEntry = - list.find((p) => p.isDefault && p.models.length > 0) ?? - list.find((p) => p.models.length > 0) - if (defaultEntry) { - updates.AI_PROVIDER = defaultEntry.provider - updates.AI_MODEL = defaultEntry.models[0] - } - } else if (list.length > 0) { - // Providers configured but no models yet — keep the list only - updates[ADMIN_PROVIDERS_KEY] = JSON.stringify(list) - } else { - updates[ADMIN_PROVIDERS_KEY] = null + updates[ADMIN_PROVIDERS_KEY] = list.length > 0 ? JSON.stringify(list) : null + + // The panel's default also becomes the server-wide default model; + // without one, the env-configured default applies. + const defaultEntry = list.find((p) => p.isDefault && p.models.length > 0) + if (defaultEntry) { + updates.AI_PROVIDER = defaultEntry.provider + updates.AI_MODEL = defaultEntry.models[0] } return updates } -// Every settings key the panel may have written for a given list +// Every settings key the panel may have written for a given list. +// AI_MODELS_CONFIG is included to clean up values written by older +// versions of the panel (it is no longer written). function derivedEnvKeys(list: StoredAdminProvider[]): string[] { const keys = new Set([ "AI_MODELS_CONFIG", @@ -267,10 +266,14 @@ function derivedEnvKeys(list: StoredAdminProvider[]): string[] { } else if (p.provider === "vertexai") { keys.add("GOOGLE_VERTEX_API_KEY") keys.add("GOOGLE_VERTEX_BASE_URL") + } else if (p.provider === "ollama") { + keys.add("OLLAMA_API_KEY") + keys.add("OLLAMA_BASE_URL") + } else { + const env = credEnvNames(p.provider, index) + if (env.key) keys.add(env.key) + if (env.url) keys.add(env.url) } - const env = credEnvNames(p.provider, index) - if (env.key) keys.add(env.key) - if (env.url) keys.add(env.url) } return [...keys] } diff --git a/lib/server-model-config.ts b/lib/server-model-config.ts index a2800d4..65ff0cc 100644 --- a/lib/server-model-config.ts +++ b/lib/server-model-config.ts @@ -62,7 +62,7 @@ function getConfigPath(): string { return path.join(process.cwd(), "ai-models.json") } -export async function loadRawServerModelsConfig(): Promise { +export async function loadEnvServerModelsConfig(): Promise { // Priority 1: AI_MODELS_CONFIG env var (JSON string) - for cloud deployments const envConfig = process.env.AI_MODELS_CONFIG if (envConfig && envConfig.trim().length > 0) { @@ -96,6 +96,40 @@ export async function loadRawServerModelsConfig(): Promise { + const envConfig = await loadEnvServerModelsConfig() + + // Merge in providers managed via the admin panel (settings.json). + // Dynamic import to avoid a module-init cycle with lib/admin/providers. + let adminConfig: ServerModelsConfig | null = null + try { + const { adminProvidersToConfig, loadAdminProviders } = await import( + "./admin/providers" + ) + const adminProviders = loadAdminProviders() + if (adminProviders.length > 0) { + adminConfig = adminProvidersToConfig(adminProviders) + } + } catch (err) { + console.error( + "[server-model-config] Failed to load admin providers:", + err, + ) + } + + if (!adminConfig || adminConfig.providers.length === 0) return envConfig + if (!envConfig) return adminConfig + + // A panel default overrides an env default + const adminHasDefault = adminConfig.providers.some((p) => p.default) + const envProviders = adminHasDefault + ? envConfig.providers.map((p) => + p.default ? { ...p, default: undefined } : p, + ) + : envConfig.providers + return { providers: [...envProviders, ...adminConfig.providers] } +} + export async function loadFlattenedServerModels(): Promise< FlattenedServerModel[] > { diff --git a/tests/unit/admin-providers.test.ts b/tests/unit/admin-providers.test.ts index 509159b..7b2cb01 100644 --- a/tests/unit/admin-providers.test.ts +++ b/tests/unit/admin-providers.test.ts @@ -3,12 +3,14 @@ import os from "os" import path from "path" import { afterEach, beforeEach, describe, expect, it } from "vitest" import { + adminProvidersToConfig, deriveEnvUpdates, mergeSecrets, type StoredAdminProvider, validateAdminProviders, } from "@/lib/admin/providers" -import { _resetForTests } from "@/lib/admin/settings" +import { _resetForTests, saveSettings } from "@/lib/admin/settings" +import { loadRawServerModelsConfig } from "@/lib/server-model-config" let tmpDir: string @@ -21,6 +23,7 @@ beforeEach(() => { afterEach(() => { _resetForTests() delete process.env.SETTINGS_FILE + delete process.env.AI_MODELS_CONFIG fs.rmSync(tmpDir, { recursive: true, force: true }) }) @@ -37,14 +40,13 @@ function provider( } describe("deriveEnvUpdates", () => { - it("writes credential env vars and AI_MODELS_CONFIG", () => { + it("writes credentials to ADMIN_-prefixed env vars (never shadows standard vars)", () => { const updates = deriveEnvUpdates([provider()], []) - expect(updates.OPENAI_API_KEY).toBe("sk-test") - const config = JSON.parse(updates.AI_MODELS_CONFIG as string) - expect(config.providers).toHaveLength(1) - expect(config.providers[0].models).toEqual(["gpt-5.2"]) - expect(updates.AI_PROVIDER).toBe("openai") - expect(updates.AI_MODEL).toBe("gpt-5.2") + expect(updates.ADMIN_OPENAI_API_KEY).toBe("sk-test") + expect(updates.OPENAI_API_KEY).toBeUndefined() + expect(JSON.parse(updates.ADMIN_PROVIDERS as string)).toHaveLength(1) + // AI_MODELS_CONFIG is no longer written (merged at read time) + expect(updates.AI_MODELS_CONFIG).toBeNull() }) it("suffixes env vars for a second instance of the same provider", () => { @@ -60,10 +62,8 @@ describe("deriveEnvUpdates", () => { ], [], ) - expect(updates.OPENAI_API_KEY).toBe("sk-test") - expect(updates.OPENAI_API_KEY_2).toBe("sk-second") - const config = JSON.parse(updates.AI_MODELS_CONFIG as string) - expect(config.providers[1].apiKeyEnv).toBe("OPENAI_API_KEY_2") + expect(updates.ADMIN_OPENAI_API_KEY).toBe("sk-test") + expect(updates.ADMIN_OPENAI_API_KEY_2).toBe("sk-second") }) it("maps bedrock credentials to AWS env vars", () => { @@ -88,12 +88,16 @@ describe("deriveEnvUpdates", () => { it("clears keys owned by the previous list when providers are removed", () => { const prev = [provider()] const updates = deriveEnvUpdates([], prev) - expect(updates.OPENAI_API_KEY).toBeNull() + expect(updates.ADMIN_OPENAI_API_KEY).toBeNull() expect(updates.AI_MODELS_CONFIG).toBeNull() expect(updates.ADMIN_PROVIDERS).toBeNull() }) - it("respects the default flag for AI_PROVIDER/AI_MODEL", () => { + it("sets AI_PROVIDER/AI_MODEL only when a default is flagged", () => { + const noDefault = deriveEnvUpdates([provider()], []) + expect(noDefault.AI_PROVIDER).toBeNull() + expect(noDefault.AI_MODEL).toBeNull() + const updates = deriveEnvUpdates( [ provider({ id: "p1" }), @@ -108,8 +112,37 @@ describe("deriveEnvUpdates", () => { ) expect(updates.AI_PROVIDER).toBe("deepseek") expect(updates.AI_MODEL).toBe("deepseek-chat") - const config = JSON.parse(updates.AI_MODELS_CONFIG as string) - expect(config.providers[1].default).toBe(true) + }) +}) + +describe("adminProvidersToConfig", () => { + it("builds a config with ADMIN_-prefixed apiKeyEnv wiring", () => { + const config = adminProvidersToConfig([provider()]) + expect(config.providers).toHaveLength(1) + expect(config.providers[0].models).toEqual(["gpt-5.2"]) + expect(config.providers[0].apiKeyEnv).toBe("ADMIN_OPENAI_API_KEY") + }) + + it("wires suffixed env vars for a second instance", () => { + const config = adminProvidersToConfig([ + provider({ id: "p1", name: "First" }), + provider({ + id: "p2", + name: "Second", + apiKey: "sk-second", + models: ["gpt-5-mini"], + }), + ]) + expect(config.providers[1].apiKeyEnv).toBe("ADMIN_OPENAI_API_KEY_2") + }) + + it("skips providers without models and carries the default flag", () => { + const config = adminProvidersToConfig([ + provider({ id: "p1", models: [] }), + provider({ id: "p2", name: "D", isDefault: true }), + ]) + expect(config.providers).toHaveLength(1) + expect(config.providers[0].default).toBe(true) }) }) @@ -147,7 +180,77 @@ describe("mergeSecrets", () => { }) }) +describe("loadRawServerModelsConfig merge", () => { + it("combines env AI_MODELS_CONFIG with panel providers", async () => { + process.env.AI_MODELS_CONFIG = JSON.stringify({ + providers: [ + { + name: "Env OpenAI", + provider: "openai", + models: ["gpt-from-env"], + default: true, + }, + ], + }) + saveSettings(deriveEnvUpdates([provider({ name: "Panel" })], [])) + + const merged = await loadRawServerModelsConfig() + expect(merged?.providers.map((p) => p.name)).toEqual([ + "Env OpenAI", + "Panel", + ]) + // Env default kept because panel set none + expect(merged?.providers[0].default).toBe(true) + }) + + it("panel default overrides the env default", async () => { + process.env.AI_MODELS_CONFIG = JSON.stringify({ + providers: [ + { + name: "Env OpenAI", + provider: "openai", + models: ["gpt-from-env"], + default: true, + }, + ], + }) + saveSettings( + deriveEnvUpdates( + [provider({ name: "Panel", isDefault: true })], + [], + ), + ) + + const merged = await loadRawServerModelsConfig() + expect(merged?.providers[0].default).toBeFalsy() + expect(merged?.providers[1].default).toBe(true) + }) + + it("returns only env config when the panel has no providers", async () => { + process.env.AI_MODELS_CONFIG = JSON.stringify({ + providers: [ + { + name: "Env Only", + provider: "openai", + models: ["gpt-from-env"], + }, + ], + }) + const merged = await loadRawServerModelsConfig() + expect(merged?.providers.map((p) => p.name)).toEqual(["Env Only"]) + }) +}) + describe("validateAdminProviders", () => { + it("rejects names clashing with env-configured providers", () => { + expect( + validateAdminProviders( + [provider({ name: "Env OpenAI" })], + ["Env OpenAI"], + ), + ).toMatch(/already defined/) + }) + it("rejects two bedrock instances", () => { const list = [ provider({ id: "p1", provider: "bedrock" }), diff --git a/tests/unit/server-model-config.test.ts b/tests/unit/server-model-config.test.ts index 1d17d31..febdd20 100644 --- a/tests/unit/server-model-config.test.ts +++ b/tests/unit/server-model-config.test.ts @@ -1,4 +1,5 @@ -import { afterEach, describe, expect, it } from "vitest" +import { afterEach, beforeEach, describe, expect, it } from "vitest" +import { _resetForTests } from "@/lib/admin/settings" import { loadFlattenedServerModels, type ServerModelsConfig, @@ -7,11 +8,20 @@ import { const ORIGINAL_ENV = { ...process.env } +beforeEach(() => { + // Isolate from any local data/settings.json (admin panel providers + // are merged into the server models config) + process.env.SETTINGS_FILE = "/nonexistent/settings.json" + _resetForTests() +}) + afterEach(() => { + _resetForTests() process.env.AI_PROVIDER = ORIGINAL_ENV.AI_PROVIDER process.env.AI_MODEL = ORIGINAL_ENV.AI_MODEL process.env.AI_MODELS_CONFIG_PATH = ORIGINAL_ENV.AI_MODELS_CONFIG_PATH process.env.AI_MODELS_CONFIG = ORIGINAL_ENV.AI_MODELS_CONFIG + delete process.env.SETTINGS_FILE }) describe("ServerModelsConfigSchema", () => {