refactor(providers): resolveEndpoint decides key and URL; getAIModel loses its second switch

Steps 4a and 4b of the provider refactor plan. resolveEndpoint(provider,
overrides) answers, by the record's credential kind, whose key a call uses
(keySource), which key, which URL, whether the URL was configured and whether
the client-chosen URL needs the redirect guard. It never throws, fetches or
logs. The five branches of getAIModel's second switch (bedrock, vertexai,
ollama, edgeone, default) are gone: Bedrock's credential selection moved
unchanged into bedrockModel(), called when the record's sdk is bedrock; every
other provider goes through resolveEndpoint and createModel, whose switch now
runs on the record's sdk field (Vertex and Ollama became cases). The three
errors for a missing Vertex key or a missing base URL stay in getAIModel with
their texts. The SSRF guard at the top reads the credential kind instead of
provider names. resolveApiKey returns whose key it found; the array apiKeyEnv
log moved to getAIModel so a later usesServerCredentials call draws no second
log line.

The behavior snapshot is unchanged, and resolveEndpoint's keySource agrees
with the pre-refactor usesServerCredentials on all 30720 input classes.
This commit is contained in:
dayuan.jiang
2026-10-12 21:17:51 +09:00
parent 932fd03a7c
commit 25a1059648
2 changed files with 363 additions and 243 deletions
+346 -243
View File
@@ -22,7 +22,13 @@ import {
loadAdminProviders,
} from "@/lib/admin/providers"
import { getEnvFallback } from "@/lib/admin/settings"
import { PROVIDER_NAMES, PROVIDERS, type ProviderName } from "@/lib/providers"
import {
PROVIDER_NAMES,
PROVIDERS,
type ProviderDef,
type ProviderName,
publicDefaultUrl,
} from "@/lib/providers"
import { isPrivateUrl, redirectGuardedFetch } from "@/lib/ssrf-protection"
import { normalizeBaseUrl, ollamaApiUrl } from "@/lib/types/model-config"
@@ -132,6 +138,24 @@ export function resolveBaseURL(
return userBaseUrl || serverBaseUrl || undefined
}
/** The provider record of a name that may not be one */
function providerDef(name: string): ProviderDef | undefined {
return Object.hasOwn(PROVIDERS, name)
? PROVIDERS[name as ProviderName]
: undefined
}
/**
* A key and whose it is: the client's own, or the server's. "server" also
* when the variable turns out empty, which the credential check reports.
*/
interface ResolvedKey {
value?: string
source: "client" | "server"
/** The apiKeyEnv entry drawn, when there were several */
selectedKeyEnv?: string
}
/**
* Resolve API key from custom env var name or default env var.
* Supports multiple API keys per provider via ai-models.json apiKeyEnv config.
@@ -145,9 +169,9 @@ export function resolveBaseURL(
*/
function resolveApiKey(
overrides: ClientOverrides | undefined,
defaultEnvVar: string,
): string | undefined {
if (overrides?.apiKey) return overrides.apiKey
defaultEnvVar: string | undefined,
): ResolvedKey {
if (overrides?.apiKey) return { value: overrides.apiKey, source: "client" }
if (overrides?.apiKeyEnv) {
// Handle array of env var names - randomly select one
@@ -162,17 +186,21 @@ function resolveApiKey(
validEnvVars[
Math.floor(Math.random() * validEnvVars.length)
]
console.log(
`[API Key Routing] Selected ${selectedEnvVar} from ${validEnvVars.length} available keys`,
)
return process.env[selectedEnvVar]
return {
value: process.env[selectedEnvVar],
source: "server",
selectedKeyEnv: selectedEnvVar,
}
}
} else {
return process.env[overrides.apiKeyEnv]
return { value: process.env[overrides.apiKeyEnv], source: "server" }
}
}
return process.env[defaultEnvVar]
return {
value: defaultEnvVar ? process.env[defaultEnvVar] : undefined,
source: "server",
}
}
/**
@@ -578,25 +606,250 @@ function bedrockRuntimeUrl(region: string): string {
return `https://bedrock-runtime.${region}.${suffix}`
}
/**
* Providers whose SDK has the official endpoint built in. The others are
* OpenAI-compatible APIs (or Anthropic) that are called at the record's
* defaultBaseUrl unless a base URL is configured.
*/
const SDK_KNOWS_ENDPOINT = new Set<ProviderName>(
PROVIDER_NAMES.filter((p) => PROVIDERS[p].sdkKnowsEndpoint),
)
export type KeySource = "client" | "server" | "none"
/** Where and how to call a provider, once credentials are resolved */
interface Endpoint {
export interface Endpoint {
apiKey?: string
/** Anthropic Bearer auth (ANTHROPIC_AUTH_TOKEN), server only */
authToken?: string
/** Azure, the server's resource only */
resourceName?: string
/** undefined: the SDK's built-in endpoint */
baseURL?: string
/** The URL came from the client or a server variable, not the record */
configuredBaseURL: boolean
/** A client-chosen, untrusted URL: wrap fetch in redirectGuardedFetch */
guarded: boolean
/** "server" is what usesServerCredentials answers true for */
keySource: KeySource
/** The apiKeyEnv entry drawn (array case); getAIModel logs it */
selectedKeyEnv?: string
headers?: Record<string, string>
/** Set by getAIModel from guarded; resolveEndpoint never fills it */
fetch?: typeof fetch
authToken?: string // Anthropic Bearer auth
resourceName?: string // Azure
// baseURL comes from the settings or env, not the provider's default
configuredBaseURL?: boolean
}
/**
* Whose key a call uses and where it goes, by the provider's credential
* kind. Reads its arguments and process.env, draws one key with Math.random
* when apiKeyEnv is an array, and never throws, fetches or logs: getAIModel
* decides what a missing key or URL means, usesServerCredentials reads
* keySource. overrides.baseUrl must already be normalized (normalizeBaseUrl
* drops one pasted endpoint path per call, so it runs once, in the caller).
* Bedrock gets only keySource: bedrockModel() builds its SDK parameters.
*/
export function resolveEndpoint(
provider: ProviderName,
overrides?: ClientOverrides,
): Endpoint {
const def = PROVIDERS[provider]
const vars = envVarsOf(provider)
const clientUrl = overrides?.baseUrl || undefined
// Requests to a base URL the client chose must not follow redirects. A
// trusted URL is the server's own (the admin Test of an entry without
// one), not a user's
const guarded = !!clientUrl && !overrides?.trustedBaseUrl
switch (def.credential) {
case "aws": {
// The client's own Bedrock API key or access key pair, else the
// server's (the admin panel's ADMIN_AWS_* keys or an IAM role)
const clientKeys = !!(
overrides?.apiKey ||
(overrides?.awsAccessKeyId && overrides?.awsSecretAccessKey)
)
return {
configuredBaseURL: false,
guarded,
keySource: clientKeys ? "client" : "server",
}
}
case "vertexKey": {
// SECURITY: a client base URL only ever gets the client's key, so
// the server's key is never sent to a client-chosen host. A client
// key only goes to the client's URL or the official one.
const apiKey = clientUrl
? overrides?.vertexApiKey || undefined
: overrides?.vertexApiKey || process.env[vars.apiKey as string]
const baseURL = resolveBaseURL(
overrides?.vertexApiKey,
clientUrl,
process.env[vars.baseUrl as string],
)
return {
apiKey,
baseURL,
configuredBaseURL: !!baseURL,
guarded,
keySource: overrides?.vertexApiKey ? "client" : "server",
}
}
case "none":
// The site's own function (EdgeOne), an OpenAI-compatible API
// without a key. The route passes the site's absolute URL and the
// cookies and access code that authenticate the call.
return {
apiKey: "edgeone",
baseURL: clientUrl || "/api/edgeai",
headers: overrides?.headers,
configuredBaseURL: !!clientUrl,
guarded,
keySource: "none",
}
case "optionalKey": {
// SECURITY: with a client base URL only the client's key is used,
// never the server's (Ollama). A user's key never goes to the
// server's URL: without a URL of their own it goes to the
// provider's cloud. The server's key goes to the server's URL
// variable (or a server model's own), else to the SDK's local
// default.
const key: ResolvedKey = clientUrl
? { value: overrides?.apiKey || undefined, source: "client" }
: resolveApiKey(overrides, vars.apiKey)
const baseURL =
clientUrl ||
(overrides?.apiKey
? def.defaultBaseUrl
: resolveBaseUrlEnv(overrides, vars.baseUrl as string))
// Only a server key costs money; a keyless local server or the
// client's own server does not. Spelled out because resolveApiKey
// says "server" for an empty variable too.
const serverKey =
overrides?.apiKeyEnv || process.env[vars.apiKey as string]
const keySource: KeySource = overrides?.apiKey
? "client"
: !clientUrl && serverKey
? "server"
: "none"
return {
apiKey: key.value,
baseURL,
configuredBaseURL: !!baseURL,
guarded,
keySource,
selectedKeyEnv: key.selectedKeyEnv,
}
}
case "apiKey": {
// An API key and a base URL from <PREFIX>_API_KEY /
// <PREFIX>_BASE_URL, or a server model's apiKeyEnv / baseUrlEnv
const key = resolveApiKey(overrides, vars.apiKey)
const configuredBaseURL = resolveBaseURL(
overrides?.apiKey,
clientUrl,
resolveBaseUrlEnv(overrides, vars.baseUrl as string),
)
// Without a configured URL an SDK that knows its endpoint gets
// none, except OpenAI with a user's key: the SDK would read the
// server's OPENAI_BASE_URL, so the official endpoint is named.
// The others get the record's https default (a local default,
// SGLang's 127.0.0.1, only fills the settings form).
const baseURL =
configuredBaseURL ||
(def.sdkKnowsEndpoint &&
!(def.sdk === "openai" && overrides?.apiKey)
? undefined
: publicDefaultUrl(provider))
return {
apiKey: key.value,
baseURL,
configuredBaseURL: !!configuredBaseURL,
guarded,
keySource: key.source,
selectedKeyEnv: key.selectedKeyEnv,
// Bearer auth when there is no API key (Anthropic)
authToken:
def.altKeyEnv && !key.value
? process.env[def.altKeyEnv]
: undefined,
// Only the server's own resource; a client key needs its URL
// (Azure)
resourceName:
def.resourceNameEnv && !overrides?.apiKey
? process.env[def.resourceNameEnv]
: undefined,
}
}
}
}
/**
* A Bedrock model from the client's credentials if it sent any (a Bedrock
* API key, or an access key pair), else the admin panel's ADMIN_AWS_* keys,
* else the default AWS credential chain (an IAM role, the AWS_* variables).
*/
function bedrockModel(
modelId: string,
overrides: ClientOverrides | undefined,
): LanguageModel {
const clientApiKey = overrides?.apiKey
const hasClientCredentials =
overrides?.awsAccessKeyId && overrides?.awsSecretAccessKey
// Keys from the admin panel. The ADMIN_ names keep them out of the
// default AWS credential chain, which other clients such as the
// DynamoDB quota manager use with their own credentials.
const adminAccessKeyId = process.env.ADMIN_AWS_ACCESS_KEY_ID
const adminSecretAccessKey = process.env.ADMIN_AWS_SECRET_ACCESS_KEY
// The region becomes part of the endpoint's host name, so a
// request's region must be a region name, or it could send the
// server's credentials to another host
if (
overrides?.awsRegion &&
!/^[a-z]{2,4}(-[a-z]+)+-\d{1,2}$/.test(overrides.awsRegion)
) {
throw Object.assign(
new Error(`Invalid AWS region "${overrides.awsRegion}"`),
{ statusCode: 400 },
)
}
const bedrockRegion =
overrides?.awsRegion ||
process.env.ADMIN_AWS_REGION ||
process.env.AWS_REGION ||
"us-west-2"
const bedrockProvider = clientApiKey
? createAmazonBedrock({
region: bedrockRegion,
apiKey: clientApiKey,
// The SDK falls back to signing with AWS keys when it
// drops a key (one of spaces): never the server's
credentialProvider: async () => {
throw new Error("The Bedrock API key is empty")
},
// Without a baseURL it reads the server's
// AWS_ENDPOINT_URL_BEDROCK_RUNTIME / AWS_ENDPOINT_URL
baseURL: bedrockRuntimeUrl(bedrockRegion),
})
: hasClientCredentials
? createAmazonBedrock({
region: bedrockRegion,
accessKeyId: overrides.awsAccessKeyId as string,
secretAccessKey: overrides.awsSecretAccessKey as string,
...(overrides?.awsSessionToken && {
sessionToken: overrides.awsSessionToken,
}),
// Without an apiKey the SDK reads the server's
// AWS_BEARER_TOKEN_BEDROCK, which wins over the keys
apiKey: "",
// Without a baseURL it reads the server's
// AWS_ENDPOINT_URL_BEDROCK_RUNTIME / AWS_ENDPOINT_URL
baseURL: bedrockRuntimeUrl(bedrockRegion),
})
: adminAccessKeyId && adminSecretAccessKey
? createAmazonBedrock({
region: bedrockRegion,
accessKeyId: adminAccessKeyId,
secretAccessKey: adminSecretAccessKey,
// The keys the admin panel's Test button checked
apiKey: "",
})
: createAmazonBedrock({
region: bedrockRegion,
credentialProvider: fromNodeProviderChain(),
})
return bedrockProvider(modelId)
}
/**
@@ -623,7 +876,10 @@ function compatibleModel(
})
}
/** Create the model for a provider. Credentials are already resolved. */
/**
* Create the model for a provider with its endpoint resolved, by the SDK
* the record names. Bedrock never comes here: getAIModel calls bedrockModel.
*/
function createModel(
provider: ProviderName,
modelId: string,
@@ -634,7 +890,7 @@ function createModel(
...(e.baseURL && { baseURL: e.baseURL }),
...(e.fetch && { fetch: e.fetch }),
}
switch (provider) {
switch (PROVIDERS[provider].sdk) {
case "openai": {
const openaiProvider = createOpenAI(opts)
// A configured base URL is usually a proxy that only has Chat
@@ -679,8 +935,6 @@ function createModel(
// Without a key or URL the SDK uses Vercel's endpoint and OIDC
return createGateway(opts)(modelId)
case "deepseek":
case "kimi":
case "mimo":
// Kimi and MiMo return reasoning_content like DeepSeek and need it
// passed back in multi-turn tool calls (MiMo answers 400 otherwise)
return createDeepSeek(opts)(modelId)
@@ -710,10 +964,21 @@ function createModel(
})(modelId)
: compatibleModel(provider, modelId, { ...e, baseURL })
}
default:
// siliconflow, sglang, modelscope, glm, qwen, qiniu, novita,
// atlascloud, edgeone
case "vertex":
// Express Mode: an API key; the URL only when one is configured
return createVertex(opts)(modelId)
case "ollama":
return createOllama({
...(e.baseURL && { baseURL: ollamaApiUrl(e.baseURL) }),
...(e.apiKey && {
headers: { Authorization: `Bearer ${e.apiKey}` },
}),
...(e.fetch && { fetch: e.fetch }),
})(modelId)
case "openai-compatible":
return compatibleModel(provider, modelId, e)
case "bedrock":
throw new Error("Bedrock models come from bedrockModel()")
}
}
@@ -734,18 +999,27 @@ export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
// If a custom baseUrl is provided, an API key MUST also be provided.
// This prevents attackers from redirecting server API keys to malicious endpoints.
// Exception: EdgeOne doesn't require API keys.
// Ollama is exempt only when no server OLLAMA_API_KEY is configured;
// when it IS configured, the outer guard also enforces client apiKey for custom baseUrls.
// Exceptions, by the provider's credential kind: a provider without keys
// (EdgeOne); a Vertex key; an optional key (Ollama) only when the server
// has no key of that provider, otherwise the client must send its own.
// A trusted URL is the server's own (the admin Test of an entry without
// one), not a user's
// one), not a user's. An unknown provider name has no kind and is
// refused here like any other, and named below.
const requested = overrides?.provider
? providerDef(overrides.provider)
: undefined
if (
overrides?.baseUrl &&
!overrides?.trustedBaseUrl &&
!overrides?.apiKey &&
!(overrides?.provider === "vertexai" && overrides?.vertexApiKey) &&
overrides?.provider !== "edgeone" &&
!(overrides?.provider === "ollama" && !process.env.OLLAMA_API_KEY)
!(requested?.credential === "vertexKey" && overrides?.vertexApiKey) &&
requested?.credential !== "none" &&
!(
requested?.credential === "optionalKey" &&
!process.env[
envVarsOf(overrides.provider as ProviderName).apiKey as string
]
)
) {
throw new Error(
`API key is required when using a custom base URL. ` +
@@ -833,222 +1107,51 @@ export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
console.log(`[AI Provider] Initializing ${provider} with model: ${modelId}`)
// Requests to a base URL the client chose must not follow redirects
const guardedFetch =
overrides?.baseUrl && !overrides.trustedBaseUrl
? redirectGuardedFetch()
: undefined
// Build provider-specific options from environment variables
let providerOptions = buildProviderOptions(provider, modelId)
let model: LanguageModel
const def = PROVIDERS[provider]
switch (provider) {
case "bedrock": {
// Use client-provided credentials if available (a Bedrock API
// key, or an access key pair), otherwise fall back to IAM/env vars
const clientApiKey = overrides?.apiKey
const hasClientCredentials =
overrides?.awsAccessKeyId && overrides?.awsSecretAccessKey
// Keys from the admin panel. The ADMIN_ names keep them out of the
// default AWS credential chain, which other clients such as the
// DynamoDB quota manager use with their own credentials.
const adminAccessKeyId = process.env.ADMIN_AWS_ACCESS_KEY_ID
const adminSecretAccessKey = process.env.ADMIN_AWS_SECRET_ACCESS_KEY
// The region becomes part of the endpoint's host name, so a
// request's region must be a region name, or it could send the
// server's credentials to another host
if (
overrides?.awsRegion &&
!/^[a-z]{2,4}(-[a-z]+)+-\d{1,2}$/.test(overrides.awsRegion)
) {
throw Object.assign(
new Error(`Invalid AWS region "${overrides.awsRegion}"`),
{ statusCode: 400 },
)
if (def.sdk === "bedrock") {
model = bedrockModel(modelId, overrides)
// Add Anthropic beta options if using Claude models via Bedrock
if (modelId.includes("anthropic.claude")) {
// Deep merge to preserve both anthropicBeta and reasoningConfig
providerOptions = {
bedrock: {
...BEDROCK_ANTHROPIC_BETA.bedrock,
...(providerOptions?.bedrock || {}),
},
}
const bedrockRegion =
overrides?.awsRegion ||
process.env.ADMIN_AWS_REGION ||
process.env.AWS_REGION ||
"us-west-2"
const bedrockProvider = clientApiKey
? createAmazonBedrock({
region: bedrockRegion,
apiKey: clientApiKey,
// The SDK falls back to signing with AWS keys when it
// drops a key (one of spaces): never the server's
credentialProvider: async () => {
throw new Error("The Bedrock API key is empty")
},
// Without a baseURL it reads the server's
// AWS_ENDPOINT_URL_BEDROCK_RUNTIME / AWS_ENDPOINT_URL
baseURL: bedrockRuntimeUrl(bedrockRegion),
})
: hasClientCredentials
? createAmazonBedrock({
region: bedrockRegion,
accessKeyId: overrides.awsAccessKeyId as string,
secretAccessKey: overrides.awsSecretAccessKey as string,
...(overrides?.awsSessionToken && {
sessionToken: overrides.awsSessionToken,
}),
// Without an apiKey the SDK reads the server's
// AWS_BEARER_TOKEN_BEDROCK, which wins over the keys
apiKey: "",
// Without a baseURL it reads the server's
// AWS_ENDPOINT_URL_BEDROCK_RUNTIME / AWS_ENDPOINT_URL
baseURL: bedrockRuntimeUrl(bedrockRegion),
})
: adminAccessKeyId && adminSecretAccessKey
? createAmazonBedrock({
region: bedrockRegion,
accessKeyId: adminAccessKeyId,
secretAccessKey: adminSecretAccessKey,
// The keys the admin panel's Test button checked
apiKey: "",
})
: createAmazonBedrock({
region: bedrockRegion,
credentialProvider: fromNodeProviderChain(),
})
model = bedrockProvider(modelId)
// Add Anthropic beta options if using Claude models via Bedrock
if (modelId.includes("anthropic.claude")) {
// Deep merge to preserve both anthropicBeta and reasoningConfig
providerOptions = {
bedrock: {
...BEDROCK_ANTHROPIC_BETA.bedrock,
...(providerOptions?.bedrock || {}),
},
}
}
break
}
case "vertexai": {
// Express Mode: Use API key for authentication
// SECURITY: a client base URL only ever gets the client's key, so the
// server's GOOGLE_VERTEX_API_KEY is never sent to a client-chosen host
const vertexApiKey = overrides?.baseUrl
? overrides.vertexApiKey
: overrides?.vertexApiKey || process.env.GOOGLE_VERTEX_API_KEY
if (!vertexApiKey) {
throw new Error(
"Vertex AI requires an API key for Express Mode. " +
"Get one from Google Cloud Console or set GOOGLE_VERTEX_API_KEY environment variable.",
)
}
// Support custom base URL from env or client override.
// A client key only goes to the client's URL or the official one.
const baseURL = resolveBaseURL(
overrides?.vertexApiKey,
overrides?.baseUrl,
process.env.GOOGLE_VERTEX_BASE_URL,
} else {
const e = resolveEndpoint(provider, overrides)
if (e.selectedKeyEnv) {
console.log(`[API Key Routing] Selected ${e.selectedKeyEnv}`)
}
if (def.credential === "vertexKey" && !e.apiKey) {
throw new Error(
"Vertex AI requires an API key for Express Mode. " +
"Get one from Google Cloud Console or set GOOGLE_VERTEX_API_KEY environment variable.",
)
model = createVertex({
apiKey: vertexApiKey,
...(baseURL && { baseURL }),
...(guardedFetch && { fetch: guardedFetch }),
})(modelId)
break
}
case "ollama": {
// SECURITY: When client provides a custom base URL, only use
// client-provided API key. Never fall back to server OLLAMA_API_KEY
// to prevent leaking server credentials to user-controlled endpoints.
const apiKey = overrides?.baseUrl
? overrides?.apiKey || undefined
: resolveApiKey(overrides, "OLLAMA_API_KEY")
// Like other providers, a user's key never goes to the server's
// base URL: without a URL of their own it goes to Ollama Cloud.
// The server's key goes to OLLAMA_BASE_URL (or a server model's
// own variable), else to the SDK's local default.
const baseURL =
overrides?.baseUrl ||
(overrides?.apiKey
? PROVIDERS.ollama.defaultBaseUrl
: resolveBaseUrlEnv(overrides, "OLLAMA_BASE_URL"))
model = createOllama({
...(baseURL && { baseURL: ollamaApiUrl(baseURL) }),
...(apiKey && {
headers: { Authorization: `Bearer ${apiKey}` },
}),
...(guardedFetch && { fetch: guardedFetch }),
})(modelId)
break
}
case "edgeone":
// EdgeOne Pages Edge AI, an OpenAI-compatible API without a key.
// The SDK appends /chat/completions to the base URL. Cookies
// (eo_token, eo_time) and the access code authenticate the call.
model = compatibleModel(provider, modelId, {
apiKey: "edgeone",
baseURL: overrides?.baseUrl || "/api/edgeai",
headers: overrides?.headers,
fetch: guardedFetch,
})
break
default: {
// Every other provider takes an API key and a base URL from
// <NAME>_API_KEY / <NAME>_BASE_URL (or a server model's apiKeyEnv)
const apiKey = resolveApiKey(
overrides,
PROVIDER_ENV_VARS[provider] as string,
// A key-only provider needs a URL unless its SDK knows the endpoint;
// with a user's key an SDK that builds the URL from the server's
// resource name (Azure) would read AZURE_RESOURCE_NAME, so it needs one too
if (
def.credential === "apiKey" &&
!e.baseURL &&
(!def.sdkKnowsEndpoint ||
(def.resourceNameEnv && overrides?.apiKey))
) {
throw new Error(
`${def.label} needs a base URL. Add it in the model settings.`,
)
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
// OPENAI_BASE_URL, so name the official endpoint.
const defaultUrl = PROVIDERS[provider].defaultBaseUrl
const publicDefault = defaultUrl?.startsWith("https://")
? defaultUrl
: undefined
const configuredBaseURL = resolveBaseURL(
overrides?.apiKey,
overrides?.baseUrl,
resolveBaseUrlEnv(overrides, baseUrlEnv),
)
const baseURL =
configuredBaseURL ||
(SDK_KNOWS_ENDPOINT.has(provider) &&
!(provider === "openai" && overrides?.apiKey)
? undefined
: publicDefault)
// With a user's Azure key the SDK would read the server's
// AZURE_RESOURCE_NAME
if (
!baseURL &&
(!SDK_KNOWS_ENDPOINT.has(provider) ||
(provider === "azure" && overrides?.apiKey))
) {
throw new Error(
`${PROVIDERS[provider].label} needs a base URL. Add it in the model settings.`,
)
}
model = createModel(provider, modelId, {
apiKey,
baseURL,
configuredBaseURL: !!configuredBaseURL,
fetch: guardedFetch,
// Bearer auth for Anthropic when there is no API key
authToken:
provider === "anthropic" && !apiKey
? process.env.ANTHROPIC_AUTH_TOKEN
: undefined,
// Only the server's own resource; a client key needs its URL
resourceName:
provider === "azure" && !overrides?.apiKey
? process.env.AZURE_RESOURCE_NAME
: undefined,
})
}
model = createModel(provider, modelId, {
...e,
fetch: e.guarded ? redirectGuardedFetch() : undefined,
})
}
return { model, providerOptions, modelId, provider }