mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 20:49:53 +08:00
refactor(providers): one place for env var names and the server credential check
Step 3 of the provider refactor plan. envVarsOf(provider) spells the key and base URL variable names from the record's envPrefix and keyEnv; the three copies of the gateway/AI_GATEWAY spelling (getAIModel's default branch, globalBaseUrl, the admin panel's credEnvNames) read it. serverCredentialError answers why the server lacks a provider's credentials, or null; detectProvider asks for null and validateProviderCredentials throws the text, so the Anthropic auth token and the Azure URL-or-resource rules exist once. The four error texts and their order are unchanged (tests/unit/server-credentials.test.ts was written against the old code first). credEnvNames reads fixedCredentials and envPrefix and is exported, with a table test over all 24 providers. Behavior snapshot unchanged.
This commit is contained in:
+10
-14
@@ -1,14 +1,11 @@
|
||||
import { z } from "zod"
|
||||
import { PROVIDERS, type ProviderName } from "@/lib/providers"
|
||||
import {
|
||||
ProviderNameSchema,
|
||||
type ServerModelsConfig,
|
||||
slugify,
|
||||
} from "@/lib/server-model-config"
|
||||
import {
|
||||
FIXED_CRED_PROVIDERS,
|
||||
PROVIDER_INFO,
|
||||
type ProviderName,
|
||||
} from "@/lib/types/model-config"
|
||||
import { FIXED_CRED_PROVIDERS, PROVIDER_INFO } from "@/lib/types/model-config"
|
||||
import { type MaskedSecret, maskSecret } from "./auth"
|
||||
import { loadSettings } from "./settings"
|
||||
|
||||
@@ -80,20 +77,19 @@ const SECRET_FIELDS = [
|
||||
"vertexApiKey",
|
||||
] as const
|
||||
|
||||
// ADMIN_-prefixed env var names for instance `index` (0-based) of a provider
|
||||
function credEnvNames(
|
||||
// ADMIN_-prefixed env var names for instance `index` (0-based) of a
|
||||
// provider. None for providers whose credentials live in fixed variables
|
||||
// (Bedrock, Vertex AI, Ollama) or that have none (EdgeOne).
|
||||
export 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 { fixedCredentials, envPrefix } = PROVIDERS[provider]
|
||||
if (fixedCredentials || envPrefix === null) return {}
|
||||
const suffix = index === 0 ? "" : `_${index + 1}`
|
||||
return {
|
||||
key: `ADMIN_${prefix}_API_KEY${suffix}`,
|
||||
url: `ADMIN_${prefix}_BASE_URL${suffix}`,
|
||||
key: `ADMIN_${envPrefix}_API_KEY${suffix}`,
|
||||
url: `ADMIN_${envPrefix}_BASE_URL${suffix}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+100
-92
@@ -438,56 +438,112 @@ function buildProviderOptions(
|
||||
return Object.keys(options).length > 0 ? options : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* The names of a provider's server key and base URL variables, from the
|
||||
* record: <PREFIX>_API_KEY (or the record's own keyEnv) and <PREFIX>_BASE_URL.
|
||||
* Empty for Bedrock and EdgeOne. Always the spelling; whether the server
|
||||
* must have the key is serverCredentialError's question.
|
||||
*/
|
||||
export function envVarsOf(provider: ProviderName): {
|
||||
apiKey?: string
|
||||
baseUrl?: string
|
||||
} {
|
||||
const { envPrefix, keyEnv } = PROVIDERS[provider]
|
||||
if (envPrefix === null) return {}
|
||||
return {
|
||||
apiKey: keyEnv ?? `${envPrefix}_API_KEY`,
|
||||
baseUrl: `${envPrefix}_BASE_URL`,
|
||||
}
|
||||
}
|
||||
|
||||
// Map of provider to required environment variable. null where the server
|
||||
// needs no key of this kind: Bedrock (the AWS SDK uses an IAM role or the
|
||||
// AWS_* variables), local Ollama, EdgeOne (the site's own function)
|
||||
const PROVIDER_ENV_VARS = Object.fromEntries(
|
||||
PROVIDER_NAMES.map((p) => {
|
||||
const { credential, envPrefix, keyEnv } = PROVIDERS[p]
|
||||
const { credential } = PROVIDERS[p]
|
||||
const required = credential === "apiKey" || credential === "vertexKey"
|
||||
return [p, required ? (keyEnv ?? `${envPrefix}_API_KEY`) : null]
|
||||
return [p, required ? (envVarsOf(p).apiKey ?? null) : null]
|
||||
}),
|
||||
) as Record<ProviderName, string | null>
|
||||
|
||||
/**
|
||||
* Why the server lacks this provider's credentials, or null when it has
|
||||
* them. detectProvider asks for null; validateProviderCredentials throws
|
||||
* the text. In order: an array apiKeyEnv passes when any of its variables
|
||||
* has a value, and nothing else is checked; without a custom variable a
|
||||
* provider with an altKeyEnv (Anthropic's ANTHROPIC_AUTH_TOKEN) accepts
|
||||
* either variable; a custom variable without a value fails, with no fall
|
||||
* back to the default one; a provider with a resourceNameEnv (Azure) then
|
||||
* needs its base URL variable, the server model's own URL variable or the
|
||||
* resource name. Providers with no required key variable pass.
|
||||
*/
|
||||
function serverCredentialError(
|
||||
provider: ProviderName,
|
||||
customApiKeyEnv?: string | string[],
|
||||
customBaseUrlEnv?: string,
|
||||
): string | null {
|
||||
if (Array.isArray(customApiKeyEnv)) {
|
||||
const hasAnyKey = customApiKeyEnv.some((envVar) => process.env[envVar])
|
||||
if (!hasAnyKey) {
|
||||
return (
|
||||
`At least one of [${customApiKeyEnv.join(", ")}] environment variables is required for ${provider} provider. ` +
|
||||
`Please set at least one in your .env.local file.`
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const { altKeyEnv, resourceNameEnv } = PROVIDERS[provider]
|
||||
const keyEnv = PROVIDER_ENV_VARS[provider]
|
||||
if (altKeyEnv && !customApiKeyEnv) {
|
||||
const hasCredential = !!(
|
||||
(keyEnv && process.env[keyEnv]) ||
|
||||
process.env[altKeyEnv]
|
||||
)
|
||||
if (!hasCredential) {
|
||||
return (
|
||||
`Either ${keyEnv} or ${altKeyEnv} environment variable is required for ${provider} provider. ` +
|
||||
`Please set one in your .env.local file.`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const requiredVar = customApiKeyEnv || keyEnv
|
||||
if (requiredVar && !process.env[requiredVar]) {
|
||||
return (
|
||||
`${requiredVar} environment variable is required for ${provider} provider. ` +
|
||||
`Please set it in your .env.local file.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// The SDK builds the URL from the resource name when there is no URL
|
||||
if (resourceNameEnv) {
|
||||
const baseUrlEnv = envVarsOf(provider).baseUrl as string
|
||||
const hasBaseUrl =
|
||||
!!process.env[baseUrlEnv] ||
|
||||
!!(customBaseUrlEnv && process.env[customBaseUrlEnv])
|
||||
const hasResourceName = !!process.env[resourceNameEnv]
|
||||
if (!hasBaseUrl && !hasResourceName) {
|
||||
return (
|
||||
`Azure requires either ${baseUrlEnv} or ${resourceNameEnv} to be set. ` +
|
||||
`Please set one in your .env.local file.`
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect provider based on available API keys
|
||||
* Returns the provider if exactly one is configured, otherwise null
|
||||
*/
|
||||
function detectProvider(): ProviderName | null {
|
||||
const configuredProviders: ProviderName[] = []
|
||||
|
||||
for (const [provider, envVar] of Object.entries(PROVIDER_ENV_VARS)) {
|
||||
if (envVar === null) {
|
||||
// Skip ollama - it doesn't require credentials
|
||||
continue
|
||||
}
|
||||
// Anthropic accepts ANTHROPIC_AUTH_TOKEN (Bearer auth) as alternative to ANTHROPIC_API_KEY
|
||||
const hasCredential =
|
||||
provider === "anthropic"
|
||||
? !!(
|
||||
process.env.ANTHROPIC_API_KEY ||
|
||||
process.env.ANTHROPIC_AUTH_TOKEN
|
||||
)
|
||||
: !!process.env[envVar]
|
||||
if (hasCredential) {
|
||||
// Azure requires additional config (baseURL or resourceName)
|
||||
if (provider === "azure") {
|
||||
const hasBaseUrl = !!process.env.AZURE_BASE_URL
|
||||
const hasResourceName = !!process.env.AZURE_RESOURCE_NAME
|
||||
if (hasBaseUrl || hasResourceName) {
|
||||
configuredProviders.push(provider as ProviderName)
|
||||
}
|
||||
} else {
|
||||
configuredProviders.push(provider as ProviderName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (configuredProviders.length === 1) {
|
||||
return configuredProviders[0]
|
||||
}
|
||||
|
||||
return null
|
||||
const configuredProviders = PROVIDER_NAMES.filter(
|
||||
(p) =>
|
||||
PROVIDER_ENV_VARS[p] !== null && serverCredentialError(p) === null,
|
||||
)
|
||||
return configuredProviders.length === 1 ? configuredProviders[0] : null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -500,54 +556,12 @@ function validateProviderCredentials(
|
||||
customApiKeyEnv?: string | string[],
|
||||
customBaseUrlEnv?: string,
|
||||
): void {
|
||||
// Handle array of env var names - at least one must be set
|
||||
if (Array.isArray(customApiKeyEnv)) {
|
||||
const hasAnyKey = customApiKeyEnv.some((envVar) => process.env[envVar])
|
||||
if (!hasAnyKey) {
|
||||
throw new Error(
|
||||
`At least one of [${customApiKeyEnv.join(", ")}] environment variables is required for ${provider} provider. ` +
|
||||
`Please set at least one in your .env.local file.`,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Anthropic accepts ANTHROPIC_AUTH_TOKEN (Bearer auth) as alternative to ANTHROPIC_API_KEY
|
||||
if (provider === "anthropic" && !customApiKeyEnv) {
|
||||
const hasCredential = !!(
|
||||
process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
|
||||
)
|
||||
if (!hasCredential) {
|
||||
throw new Error(
|
||||
`Either ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable is required for anthropic provider. ` +
|
||||
`Please set one in your .env.local file.`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Use custom env var name if provided, otherwise use default
|
||||
const requiredVar = customApiKeyEnv || PROVIDER_ENV_VARS[provider]
|
||||
if (requiredVar && !process.env[requiredVar]) {
|
||||
throw new Error(
|
||||
`${requiredVar} environment variable is required for ${provider} provider. ` +
|
||||
`Please set it in your .env.local file.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Azure requires either AZURE_BASE_URL or AZURE_RESOURCE_NAME in addition
|
||||
// to API key, or a server model's own URL variable (an admin panel entry)
|
||||
if (provider === "azure") {
|
||||
const hasBaseUrl =
|
||||
!!process.env.AZURE_BASE_URL ||
|
||||
!!(customBaseUrlEnv && process.env[customBaseUrlEnv])
|
||||
const hasResourceName = !!process.env.AZURE_RESOURCE_NAME
|
||||
if (!hasBaseUrl && !hasResourceName) {
|
||||
throw new Error(
|
||||
`Azure requires either AZURE_BASE_URL or AZURE_RESOURCE_NAME to be set. ` +
|
||||
`Please set one in your .env.local file.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
const error = serverCredentialError(
|
||||
provider,
|
||||
customApiKeyEnv,
|
||||
customBaseUrlEnv,
|
||||
)
|
||||
if (error) throw new Error(error)
|
||||
}
|
||||
|
||||
/** AWS's Bedrock endpoint for a region, as the Bedrock SDK builds it */
|
||||
@@ -987,10 +1001,7 @@ export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
|
||||
overrides,
|
||||
PROVIDER_ENV_VARS[provider] as string,
|
||||
)
|
||||
const baseUrlEnv =
|
||||
provider === "gateway"
|
||||
? "AI_GATEWAY_BASE_URL"
|
||||
: `${provider.toUpperCase()}_BASE_URL`
|
||||
const baseUrlEnv = envVarsOf(provider).baseUrl as string
|
||||
// A local default (SGLang's 127.0.0.1) only fills the settings
|
||||
// form; the server must not call its own machine for it. With a
|
||||
// user's key the OpenAI SDK would read the server's
|
||||
@@ -1078,10 +1089,7 @@ export function globalBaseUrl(provider: ProviderName): string | undefined {
|
||||
) {
|
||||
return `https://${process.env.AZURE_RESOURCE_NAME}.openai.azure.com/openai`
|
||||
}
|
||||
const name =
|
||||
provider === "gateway"
|
||||
? "AI_GATEWAY_BASE_URL"
|
||||
: `${provider.toUpperCase()}_BASE_URL`
|
||||
const name = envVarsOf(provider).baseUrl as string
|
||||
return process.env[name] || undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import {
|
||||
ADMIN_PROVIDERS_KEY,
|
||||
adminProvidersToConfig,
|
||||
credEnvNames,
|
||||
deriveEnvUpdates,
|
||||
loadAdminProviders,
|
||||
maskAdminProviders,
|
||||
@@ -471,3 +472,55 @@ describe("loadAdminProviders", () => {
|
||||
expect(() => maskAdminProviders(loaded)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("credEnvNames", () => {
|
||||
it("names the panel's variables for every provider", () => {
|
||||
// bedrock, vertexai and ollama write fixed variables; EdgeOne has none
|
||||
const expected: Record<string, string | null> = {
|
||||
openai: "OPENAI",
|
||||
anthropic: "ANTHROPIC",
|
||||
google: "GOOGLE",
|
||||
deepseek: "DEEPSEEK",
|
||||
qwen: "QWEN",
|
||||
doubao: "DOUBAO",
|
||||
glm: "GLM",
|
||||
kimi: "KIMI",
|
||||
minimax: "MINIMAX",
|
||||
mimo: "MIMO",
|
||||
openrouter: "OPENROUTER",
|
||||
aihubmix: "AIHUBMIX",
|
||||
siliconflow: "SILICONFLOW",
|
||||
modelscope: "MODELSCOPE",
|
||||
novita: "NOVITA",
|
||||
atlascloud: "ATLASCLOUD",
|
||||
qiniu: "QINIU",
|
||||
gateway: "AI_GATEWAY",
|
||||
bedrock: null,
|
||||
vertexai: null,
|
||||
azure: "AZURE",
|
||||
edgeone: null,
|
||||
ollama: null,
|
||||
sglang: "SGLANG",
|
||||
}
|
||||
for (const [p, prefix] of Object.entries(expected)) {
|
||||
const first = credEnvNames(p as StoredAdminProvider["provider"], 0)
|
||||
const third = credEnvNames(p as StoredAdminProvider["provider"], 2)
|
||||
expect([p, first, third]).toEqual([
|
||||
p,
|
||||
prefix
|
||||
? {
|
||||
key: `ADMIN_${prefix}_API_KEY`,
|
||||
url: `ADMIN_${prefix}_BASE_URL`,
|
||||
}
|
||||
: {},
|
||||
prefix
|
||||
? {
|
||||
key: `ADMIN_${prefix}_API_KEY_3`,
|
||||
url: `ADMIN_${prefix}_BASE_URL_3`,
|
||||
}
|
||||
: {},
|
||||
])
|
||||
}
|
||||
expect(Object.keys(expected)).toHaveLength(24)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* The server credential check getAIModel runs when the request brings no
|
||||
* key of its own, and the auto-detection of the server's provider. Written
|
||||
* against the pre-refactor validateProviderCredentials and detectProvider;
|
||||
* the refactor merges both into serverCredentialError, which must keep every
|
||||
* answer and every error text below.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { getAIModel, getServerProvider } from "@/lib/ai-providers"
|
||||
|
||||
vi.mock("@/lib/admin/settings", () => ({
|
||||
loadSettings: () => ({}),
|
||||
getEnvFallback: (key: string) => process.env[key] ?? null,
|
||||
}))
|
||||
|
||||
const { factory } = vi.hoisted(() => ({
|
||||
factory: () => (modelId: string) => ({ modelId }),
|
||||
}))
|
||||
vi.mock("@ai-sdk/openai", () => ({ createOpenAI: factory }))
|
||||
vi.mock("@ai-sdk/anthropic", () => ({ createAnthropic: factory }))
|
||||
vi.mock("@ai-sdk/azure", () => ({ createAzure: factory }))
|
||||
vi.mock("@ai-sdk/google-vertex", () => ({ createVertex: factory }))
|
||||
vi.mock("ollama-ai-provider-v2", () => ({ createOllama: factory }))
|
||||
|
||||
const VARS = [
|
||||
"AI_PROVIDER",
|
||||
"AI_MODEL",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"AZURE_API_KEY",
|
||||
"AZURE_BASE_URL",
|
||||
"AZURE_RESOURCE_NAME",
|
||||
"OPENAI_API_KEY",
|
||||
"OPENAI_BASE_URL",
|
||||
"GOOGLE_VERTEX_API_KEY",
|
||||
"OLLAMA_API_KEY",
|
||||
"MY_KEY",
|
||||
"MY_KEY_2",
|
||||
"MY_URL",
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
for (const name of VARS) vi.stubEnv(name, undefined)
|
||||
vi.spyOn(console, "log").mockImplementation(() => {})
|
||||
})
|
||||
|
||||
const run = (overrides: Parameters<typeof getAIModel>[0]) => () =>
|
||||
getAIModel({ modelId: "m", ...overrides })
|
||||
|
||||
describe("server credentials", () => {
|
||||
it("accepts ANTHROPIC_AUTH_TOKEN alone for Anthropic", () => {
|
||||
vi.stubEnv("ANTHROPIC_AUTH_TOKEN", "token")
|
||||
expect(run({ provider: "anthropic" })).not.toThrow()
|
||||
vi.stubEnv("ANTHROPIC_AUTH_TOKEN", undefined)
|
||||
expect(run({ provider: "anthropic" })).toThrow(
|
||||
"Either ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable is required for anthropic provider. Please set one in your .env.local file.",
|
||||
)
|
||||
})
|
||||
|
||||
it("holds a server model to its own variable, even for Anthropic", () => {
|
||||
vi.stubEnv("ANTHROPIC_API_KEY", "k")
|
||||
vi.stubEnv("ANTHROPIC_AUTH_TOKEN", "token")
|
||||
expect(run({ provider: "anthropic", apiKeyEnv: "MY_KEY" })).toThrow(
|
||||
"MY_KEY environment variable is required for anthropic provider. Please set it in your .env.local file.",
|
||||
)
|
||||
})
|
||||
|
||||
it("does not fall back to the default variable from an empty custom one", () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "k")
|
||||
expect(run({ provider: "openai", apiKeyEnv: "MY_KEY" })).toThrow(
|
||||
"MY_KEY environment variable is required for openai provider. Please set it in your .env.local file.",
|
||||
)
|
||||
vi.stubEnv("MY_KEY", "custom")
|
||||
expect(run({ provider: "openai", apiKeyEnv: "MY_KEY" })).not.toThrow()
|
||||
})
|
||||
|
||||
it("needs a URL or a resource name for Azure after the key", () => {
|
||||
expect(run({ provider: "azure" })).toThrow(
|
||||
"AZURE_API_KEY environment variable is required for azure provider. Please set it in your .env.local file.",
|
||||
)
|
||||
vi.stubEnv("AZURE_API_KEY", "k")
|
||||
expect(run({ provider: "azure" })).toThrow(
|
||||
"Azure requires either AZURE_BASE_URL or AZURE_RESOURCE_NAME to be set. Please set one in your .env.local file.",
|
||||
)
|
||||
vi.stubEnv("AZURE_RESOURCE_NAME", "res")
|
||||
expect(run({ provider: "azure" })).not.toThrow()
|
||||
})
|
||||
|
||||
it("lets a server model's own URL variable stand in for Azure's", () => {
|
||||
vi.stubEnv("MY_KEY", "k")
|
||||
vi.stubEnv("MY_URL", "https://res.openai.azure.com/openai")
|
||||
expect(
|
||||
run({
|
||||
provider: "azure",
|
||||
apiKeyEnv: "MY_KEY",
|
||||
baseUrlEnv: "MY_URL",
|
||||
}),
|
||||
).not.toThrow()
|
||||
vi.stubEnv("MY_URL", undefined)
|
||||
expect(
|
||||
run({
|
||||
provider: "azure",
|
||||
apiKeyEnv: "MY_KEY",
|
||||
baseUrlEnv: "MY_URL",
|
||||
}),
|
||||
).toThrow(/AZURE_BASE_URL or AZURE_RESOURCE_NAME/)
|
||||
})
|
||||
|
||||
it("passes an array apiKeyEnv on any value and checks nothing else", () => {
|
||||
vi.stubEnv("MY_KEY_2", "k")
|
||||
// No Azure URL anywhere, and the key check still passes
|
||||
expect(
|
||||
run({ provider: "azure", apiKeyEnv: ["MY_KEY", "MY_KEY_2"] }),
|
||||
).not.toThrow()
|
||||
vi.stubEnv("MY_KEY_2", undefined)
|
||||
expect(
|
||||
run({ provider: "azure", apiKeyEnv: ["MY_KEY", "MY_KEY_2"] }),
|
||||
).toThrow(
|
||||
"At least one of [MY_KEY, MY_KEY_2] environment variables is required for azure provider. Please set at least one in your .env.local file.",
|
||||
)
|
||||
})
|
||||
|
||||
it("asks nothing of providers without a required key variable", () => {
|
||||
expect(run({ provider: "ollama" })).not.toThrow()
|
||||
// A custom variable is still checked
|
||||
expect(run({ provider: "ollama", apiKeyEnv: "MY_KEY" })).toThrow(
|
||||
"MY_KEY environment variable is required for ollama provider. Please set it in your .env.local file.",
|
||||
)
|
||||
})
|
||||
|
||||
it("names Vertex AI's key variable", () => {
|
||||
expect(run({ provider: "vertexai" })).toThrow(
|
||||
"GOOGLE_VERTEX_API_KEY environment variable is required for vertexai provider. Please set it in your .env.local file.",
|
||||
)
|
||||
vi.stubEnv("GOOGLE_VERTEX_API_KEY", "k")
|
||||
expect(run({ provider: "vertexai" })).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("server provider auto-detection", () => {
|
||||
it("finds the one provider with a key", () => {
|
||||
vi.stubEnv("GOOGLE_VERTEX_API_KEY", "k")
|
||||
expect(getServerProvider()).toBe("vertexai")
|
||||
expect(run({})).not.toThrow()
|
||||
})
|
||||
|
||||
it("needs AI_PROVIDER when two have keys", () => {
|
||||
vi.stubEnv("GOOGLE_VERTEX_API_KEY", "k")
|
||||
vi.stubEnv("OPENAI_API_KEY", "k")
|
||||
expect(getServerProvider()).toBeNull()
|
||||
expect(run({})).toThrow(
|
||||
"Multiple AI providers configured (openai, vertexai). Please set AI_PROVIDER to specify which one to use.",
|
||||
)
|
||||
})
|
||||
|
||||
it("accepts AI_PROVIDER=ollama with no key at all", () => {
|
||||
vi.stubEnv("AI_PROVIDER", "ollama")
|
||||
expect(getServerProvider()).toBe("ollama")
|
||||
expect(run({})).not.toThrow()
|
||||
})
|
||||
|
||||
it("only counts Azure with a URL or a resource name", () => {
|
||||
vi.stubEnv("AZURE_API_KEY", "k")
|
||||
expect(getServerProvider()).toBeNull()
|
||||
vi.stubEnv("AZURE_RESOURCE_NAME", "res")
|
||||
expect(getServerProvider()).toBe("azure")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user