Merge branch 'main' into feat/gcp-vertex-ai

This commit is contained in:
ElshadHu
2026-01-14 01:09:52 -05:00
9 changed files with 227 additions and 91 deletions

View File

@@ -67,6 +67,32 @@ const ANTHROPIC_BETA_HEADERS = {
"anthropic-beta": "fine-grained-tool-streaming-2025-05-14",
}
/**
* Resolve baseURL based on whether user is providing their own API key.
* When user provides their own API key, we should NOT fall back to server's
* baseURL environment variable - user credentials should only be sent to
* user-specified endpoints or official provider endpoints.
*
* @param userApiKey - User-provided API key (if any)
* @param userBaseUrl - User-provided base URL (if any)
* @param serverBaseUrl - Server's base URL from environment variable
* @param defaultBaseUrl - Provider's official/default base URL (optional)
* @returns The resolved base URL to use
*/
export function resolveBaseURL(
userApiKey: string | null | undefined,
userBaseUrl: string | null | undefined,
serverBaseUrl: string | undefined,
defaultBaseUrl?: string,
): string | undefined {
if (userApiKey) {
// User provides their own API key - only use user's baseUrl or default
return userBaseUrl || defaultBaseUrl || undefined
}
// No user API key - fall back to server config
return userBaseUrl || serverBaseUrl || defaultBaseUrl || undefined
}
/**
* Safely parse integer from environment variable with validation
*/
@@ -635,7 +661,11 @@ 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
const baseURL = resolveBaseURL(
overrides?.apiKey,
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)
@@ -654,10 +684,12 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "anthropic": {
const apiKey = overrides?.apiKey || process.env.ANTHROPIC_API_KEY
const baseURL =
overrides?.baseUrl ||
process.env.ANTHROPIC_BASE_URL ||
"https://api.anthropic.com/v1"
const baseURL = resolveBaseURL(
overrides?.apiKey,
overrides?.baseUrl,
process.env.ANTHROPIC_BASE_URL,
"https://api.anthropic.com/v1",
)
const customProvider = createAnthropic({
apiKey,
baseURL,
@@ -672,7 +704,11 @@ 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
const baseURL = resolveBaseURL(
overrides?.apiKey,
overrides?.baseUrl,
process.env.GOOGLE_BASE_URL,
)
if (baseURL || overrides?.apiKey) {
const customGoogle = createGoogleGenerativeAI({
apiKey,
@@ -711,8 +747,15 @@ 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
const baseURL = resolveBaseURL(
overrides?.apiKey,
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) {
@@ -742,8 +785,11 @@ 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
const baseURL = resolveBaseURL(
overrides?.apiKey,
overrides?.baseUrl,
process.env.OPENROUTER_BASE_URL,
)
const openrouter = createOpenRouter({
apiKey,
...(baseURL && { baseURL }),
@@ -754,7 +800,11 @@ 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
const baseURL = resolveBaseURL(
overrides?.apiKey,
overrides?.baseUrl,
process.env.DEEPSEEK_BASE_URL,
)
if (baseURL || overrides?.apiKey) {
const customDeepSeek = createDeepSeek({
apiKey,
@@ -769,10 +819,12 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "siliconflow": {
const apiKey = overrides?.apiKey || process.env.SILICONFLOW_API_KEY
const baseURL =
overrides?.baseUrl ||
process.env.SILICONFLOW_BASE_URL ||
"https://api.siliconflow.cn/v1"
const baseURL = resolveBaseURL(
overrides?.apiKey,
overrides?.baseUrl,
process.env.SILICONFLOW_BASE_URL,
"https://api.siliconflow.cn/v1",
)
const siliconflowProvider = createOpenAI({
apiKey,
baseURL,
@@ -783,11 +835,15 @@ 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
const baseURL = resolveBaseURL(
overrides?.apiKey,
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)
@@ -892,8 +948,11 @@ 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
const baseURL = resolveBaseURL(
overrides?.apiKey,
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) {
@@ -925,10 +984,12 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "doubao": {
const apiKey = overrides?.apiKey || process.env.DOUBAO_API_KEY
const baseURL =
overrides?.baseUrl ||
process.env.DOUBAO_BASE_URL ||
"https://ark.cn-beijing.volces.com/api/v3"
const baseURL = resolveBaseURL(
overrides?.apiKey,
overrides?.baseUrl,
process.env.DOUBAO_BASE_URL,
"https://ark.cn-beijing.volces.com/api/v3",
)
const lowerModelId = modelId.toLowerCase()
// Use DeepSeek provider for DeepSeek/Kimi models, OpenAI for others (multimodal support)
if (
@@ -952,10 +1013,12 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
case "modelscope": {
const apiKey = overrides?.apiKey || process.env.MODELSCOPE_API_KEY
const baseURL =
overrides?.baseUrl ||
process.env.MODELSCOPE_BASE_URL ||
"https://api-inference.modelscope.cn/v1"
const baseURL = resolveBaseURL(
overrides?.apiKey,
overrides?.baseUrl,
process.env.MODELSCOPE_BASE_URL,
"https://api-inference.modelscope.cn/v1",
)
const modelscopeProvider = createOpenAI({
apiKey,
baseURL,

View File

@@ -100,8 +100,6 @@
"switchTo": "Switch to",
"minimal": "Minimal",
"sketch": "Sketch",
"closeProtection": "Close Protection",
"closeProtectionDescription": "Show confirmation when leaving the page.",
"diagramStyle": "Diagram Style",
"diagramStyleDescription": "Toggle between minimal and styled diagram output.",
"sendShortcut": "Send Shortcut",

View File

@@ -100,8 +100,6 @@
"switchTo": "切り替え",
"minimal": "ミニマル",
"sketch": "スケッチ",
"closeProtection": "ページ離脱確認",
"closeProtectionDescription": "ページを離れる際に確認を表示します。",
"diagramStyle": "ダイアグラムスタイル",
"diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
"sendShortcut": "送信ショートカット",

View File

@@ -100,8 +100,6 @@
"switchTo": "切换到",
"minimal": "简约",
"sketch": "草图",
"closeProtection": "关闭确认",
"closeProtectionDescription": "离开页面时显示确认。",
"diagramStyle": "图表样式",
"diagramStyleDescription": "切换简约与精致图表输出模式。",
"sendShortcut": "发送快捷键",

View File

@@ -12,7 +12,6 @@ export const STORAGE_KEYS = {
// Settings
accessCode: "next-ai-draw-io-access-code",
closeProtection: "next-ai-draw-io-close-protection",
accessCodeRequired: "next-ai-draw-io-access-code-required",
aiProvider: "next-ai-draw-io-ai-provider",
aiBaseUrl: "next-ai-draw-io-ai-base-url",