fix: prevent user API keys from using server's baseURL

When users provide their own API key but not a custom baseURL,
the code was incorrectly falling back to the server's environment
variable for baseURL. This caused user API keys to be sent to
the server's custom proxy endpoint instead of the provider's
official endpoint, resulting in 'API key format incorrect' errors.

This fix ensures that when a user provides their own API key:
- Only the user's baseUrl is used (if provided)
- Otherwise, the provider's official/default endpoint is used
- Server's baseURL env vars are never mixed with user credentials

Affected providers: openai, anthropic, google, azure, openrouter,
deepseek, siliconflow, sglang, gateway, doubao, modelscope

Also fixes Azure's resourceName to not leak server config to user keys.

Fixes #577
This commit is contained in:
dayuan.jiang
2026-01-13 22:03:41 +09:00
parent 0d79487b6c
commit d5774b336c
+50 -19
View File
@@ -595,7 +595,10 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "openai": {
const apiKey = overrides?.apiKey || process.env.OPENAI_API_KEY
const baseURL = overrides?.baseUrl || process.env.OPENAI_BASE_URL
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl
: overrides?.baseUrl || process.env.OPENAI_BASE_URL
if (baseURL) {
// Custom base URL = third-party proxy, use Chat Completions API
// for compatibility (most proxies don't support /responses endpoint)
@@ -614,8 +617,10 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "anthropic": {
const apiKey = overrides?.apiKey || process.env.ANTHROPIC_API_KEY
const baseURL =
overrides?.baseUrl ||
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl || "https://api.anthropic.com/v1"
: overrides?.baseUrl ||
process.env.ANTHROPIC_BASE_URL ||
"https://api.anthropic.com/v1"
const customProvider = createAnthropic({
@@ -632,7 +637,10 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "google": {
const apiKey =
overrides?.apiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY
const baseURL = overrides?.baseUrl || process.env.GOOGLE_BASE_URL
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl
: overrides?.baseUrl || process.env.GOOGLE_BASE_URL
if (baseURL || overrides?.apiKey) {
const customGoogle = createGoogleGenerativeAI({
apiKey,
@@ -647,8 +655,14 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "azure": {
const apiKey = overrides?.apiKey || process.env.AZURE_API_KEY
const baseURL = overrides?.baseUrl || process.env.AZURE_BASE_URL
const resourceName = process.env.AZURE_RESOURCE_NAME
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl
: overrides?.baseUrl || process.env.AZURE_BASE_URL
// Only use server's resourceName if user is NOT providing their own API key
const resourceName = overrides?.apiKey
? undefined
: process.env.AZURE_RESOURCE_NAME
// Azure requires either baseURL or resourceName to construct the endpoint
// resourceName constructs: https://{resourceName}.openai.azure.com/openai/v1{path}
if (baseURL || resourceName || overrides?.apiKey) {
@@ -678,8 +692,10 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "openrouter": {
const apiKey = overrides?.apiKey || process.env.OPENROUTER_API_KEY
const baseURL =
overrides?.baseUrl || process.env.OPENROUTER_BASE_URL
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl
: overrides?.baseUrl || process.env.OPENROUTER_BASE_URL
const openrouter = createOpenRouter({
apiKey,
...(baseURL && { baseURL }),
@@ -690,7 +706,10 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "deepseek": {
const apiKey = overrides?.apiKey || process.env.DEEPSEEK_API_KEY
const baseURL = overrides?.baseUrl || process.env.DEEPSEEK_BASE_URL
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl
: overrides?.baseUrl || process.env.DEEPSEEK_BASE_URL
if (baseURL || overrides?.apiKey) {
const customDeepSeek = createDeepSeek({
apiKey,
@@ -705,8 +724,10 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "siliconflow": {
const apiKey = overrides?.apiKey || process.env.SILICONFLOW_API_KEY
const baseURL =
overrides?.baseUrl ||
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl || "https://api.siliconflow.cn/v1"
: overrides?.baseUrl ||
process.env.SILICONFLOW_BASE_URL ||
"https://api.siliconflow.cn/v1"
const siliconflowProvider = createOpenAI({
@@ -719,11 +740,14 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "sglang": {
const apiKey = overrides?.apiKey || process.env.SGLANG_API_KEY
const baseURL = overrides?.baseUrl || process.env.SGLANG_BASE_URL
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl
: overrides?.baseUrl || process.env.SGLANG_BASE_URL
const sglangProvider = createOpenAI({
apiKey,
baseURL,
...(baseURL && { baseURL }),
// Add a custom fetch wrapper to intercept and fix the stream from sglang
fetch: async (url, options) => {
const response = await fetch(url, options)
@@ -828,8 +852,10 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
// Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
// See: https://vercel.com/ai-gateway
const apiKey = overrides?.apiKey || process.env.AI_GATEWAY_API_KEY
const baseURL =
overrides?.baseUrl || process.env.AI_GATEWAY_BASE_URL
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl
: overrides?.baseUrl || process.env.AI_GATEWAY_BASE_URL
// Only use custom configuration if explicitly set (local dev or custom Gateway)
// Otherwise undefined → AI SDK uses Vercel default (https://ai-gateway.vercel.sh/v1/ai) + OIDC
if (baseURL || overrides?.apiKey) {
@@ -861,8 +887,11 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "doubao": {
const apiKey = overrides?.apiKey || process.env.DOUBAO_API_KEY
const baseURL =
overrides?.baseUrl ||
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl ||
"https://ark.cn-beijing.volces.com/api/v3"
: overrides?.baseUrl ||
process.env.DOUBAO_BASE_URL ||
"https://ark.cn-beijing.volces.com/api/v3"
const lowerModelId = modelId.toLowerCase()
@@ -888,8 +917,10 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "modelscope": {
const apiKey = overrides?.apiKey || process.env.MODELSCOPE_API_KEY
const baseURL =
overrides?.baseUrl ||
// Only fall back to server's baseURL if user is NOT providing their own API key
const baseURL = overrides?.apiKey
? overrides?.baseUrl || "https://api-inference.modelscope.cn/v1"
: overrides?.baseUrl ||
process.env.MODELSCOPE_BASE_URL ||
"https://api-inference.modelscope.cn/v1"
const modelscopeProvider = createOpenAI({