fix(provider): auto backfill default base urls on migration
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-19 00:06:11 +08:00
parent bdf980044a
commit b7de92ec4e
3 changed files with 89 additions and 32 deletions

View File

@ -22,6 +22,7 @@ import { createRuxaImage } from "./providers/ruxa-image";
import { createFalImage } from "./providers/fal-image";
import { createFalVideo } from "./providers/fal-video";
import { createVolcanoMultimodal } from "./providers/volcano-multimodal";
import { getProviderDefaultBaseUrl } from "./provider-defaults";
// 缓存已创建的 provider 实例
const providerCache = new Map<string, ReturnType<typeof createOpenAI>>();
@ -41,7 +42,9 @@ function isVolcanoMultimodalBaseUrl(baseUrl?: string | null) {
* - custom: OpenAI兼容的第三方API (GPT-5 Nano、其他兼容服务等)
*/
function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl" | "apiKey">) {
const cacheKey = `${provider.id}-${provider.baseUrl}`;
const resolvedBaseUrl =
provider.baseUrl?.trim() || getProviderDefaultBaseUrl(provider.type) || "";
const cacheKey = `${provider.id}-${resolvedBaseUrl}`;
if (providerCache.has(cacheKey)) {
return providerCache.get(cacheKey);
@ -57,9 +60,9 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
// 阿里云提供 OpenAI 兼容的接口:https://dashscope.aliyuncs.com/compatible-mode/v1
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
});
console.log(`[Provider] Created Alibaba DashScope provider: ${provider.baseUrl}`);
console.log(`[Provider] Created Alibaba DashScope provider: ${resolvedBaseUrl}`);
break;
}
@ -68,19 +71,19 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
// 官方 API 地址:https://api.openai.com/v1
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: provider.baseUrl || "https://api.openai.com/v1",
baseURL: resolvedBaseUrl || "https://api.openai.com/v1",
});
console.log(`[Provider] Created OpenAI provider: ${provider.baseUrl || "https://api.openai.com/v1"}`);
console.log(`[Provider] Created OpenAI provider: ${resolvedBaseUrl || "https://api.openai.com/v1"}`);
break;
}
case "zhipu": {
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: provider.baseUrl || "https://open.bigmodel.cn/api/paas/v4",
baseURL: resolvedBaseUrl || "https://open.bigmodel.cn/api/paas/v4",
});
console.log(
`[Provider] Created Zhipu Open Platform provider: ${provider.baseUrl || "https://open.bigmodel.cn/api/paas/v4"}`
`[Provider] Created Zhipu Open Platform provider: ${resolvedBaseUrl || "https://open.bigmodel.cn/api/paas/v4"}`
);
break;
}
@ -90,10 +93,10 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
// 使用自定义的本地模型接口,不兼容 OpenAI 格式
// 本地模型不使用缓存,因为类型不兼容 OpenAIProvider
const localProvider = createLocalQwen({
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created local model provider: ${provider.baseUrl}`);
console.log(`[Provider] Created local model provider: ${resolvedBaseUrl}`);
return localProvider as any;
}
@ -101,10 +104,10 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
// Gemini 图像生成中转服务
// 使用特殊的响应格式处理(Markdown + inline_data)
const geminiProvider = createGeminiImage({
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Gemini image generation provider: ${provider.baseUrl}`);
console.log(`[Provider] Created Gemini image generation provider: ${resolvedBaseUrl}`);
return geminiProvider as any;
}
@ -112,10 +115,10 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
// 环科 Gemini 原生格式 API
// 使用 /v1beta/models/{model}:generateContent 端点
const huankeGeminiProvider = createHuankeGemini({
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Huanke Gemini image generation provider: ${provider.baseUrl}`);
console.log(`[Provider] Created Huanke Gemini image generation provider: ${resolvedBaseUrl}`);
return huankeGeminiProvider as any;
}
@ -123,20 +126,20 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
// 千问视频生成服务(通义万相 I2V)
// 支持图片转视频功能,异步任务轮询
const qianwenVideoProvider = createQianwenVideo({
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Qianwen video generation provider: ${provider.baseUrl}`);
console.log(`[Provider] Created Qianwen video generation provider: ${resolvedBaseUrl}`);
return qianwenVideoProvider as any;
}
case "doubao-video": {
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: provider.baseUrl || "https://ark.cn-beijing.volces.com/api/v3",
baseURL: resolvedBaseUrl || "https://ark.cn-beijing.volces.com/api/v3",
});
console.log(
`[Provider] Created Doubao video provider: ${provider.baseUrl || "https://ark.cn-beijing.volces.com/api/v3"}`
`[Provider] Created Doubao video provider: ${resolvedBaseUrl || "https://ark.cn-beijing.volces.com/api/v3"}`
);
break;
}
@ -144,10 +147,10 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
case "fal-video": {
// FAL 视频生成服务(Kling O3 等)
const falVideoProvider = createFalVideo({
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Fal Video generation provider: ${provider.baseUrl}`);
console.log(`[Provider] Created Fal Video generation provider: ${resolvedBaseUrl}`);
return falVideoProvider as any;
}
@ -155,10 +158,10 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
// 火山引擎视觉图片生成服务(Seedream)
// 支持文生图、图生图、多图融合等功能
const volcanoVisionProvider = createVolcanoVision({
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Volcano Vision image generation provider: ${provider.baseUrl}`);
console.log(`[Provider] Created Volcano Vision image generation provider: ${resolvedBaseUrl}`);
return volcanoVisionProvider as any;
}
@ -166,42 +169,42 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
// 阿里云千问图片生成/编辑服务
// 支持 qwen-image-edit-plus(1-6张输出)和 qwen-image-edit(1张输出)
const alibabaImageProvider = createAlibabaImage({
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Alibaba Image generation provider: ${provider.baseUrl}`);
console.log(`[Provider] Created Alibaba Image generation provider: ${resolvedBaseUrl}`);
return alibabaImageProvider as any;
}
case "ruxa-image": {
// RUXA 图片编辑服务
const ruxaImageProvider = createRuxaImage({
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Ruxa Image generation provider: ${provider.baseUrl}`);
console.log(`[Provider] Created Ruxa Image generation provider: ${resolvedBaseUrl}`);
return ruxaImageProvider as any;
}
case "fal-image": {
// FAL 图片编辑服务(Nano Banana Pro)
const falImageProvider = createFalImage({
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Fal Image generation provider: ${provider.baseUrl}`);
console.log(`[Provider] Created Fal Image generation provider: ${resolvedBaseUrl}`);
return falImageProvider as any;
}
case "custom":
default: {
if (isVolcanoMultimodalBaseUrl(provider.baseUrl)) {
if (isVolcanoMultimodalBaseUrl(resolvedBaseUrl)) {
const volcanoMultimodalProvider = createVolcanoMultimodal({
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(
`[Provider] Created Volcano Multimodal provider via Responses API: ${provider.baseUrl}`
`[Provider] Created Volcano Multimodal provider via Responses API: ${resolvedBaseUrl}`
);
return volcanoMultimodalProvider as any;
}
@ -210,9 +213,11 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
// 请求格式遵循 OpenAI Chat Completions 标准:/v1/chat/completions
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: provider.baseUrl,
baseURL: resolvedBaseUrl || provider.baseUrl,
});
console.log(`[Provider] Created custom OpenAI-compatible provider: ${provider.baseUrl}`);
console.log(
`[Provider] Created custom OpenAI-compatible provider: ${resolvedBaseUrl || provider.baseUrl}`
);
break;
}
}

View File

@ -0,0 +1,21 @@
import type { AiProvider } from "@/lib/db/schema";
const PROVIDER_DEFAULT_BASE_URL: Partial<Record<AiProvider["type"], string>> = {
alibaba: "https://dashscope.aliyuncs.com/compatible-mode/v1",
openai: "https://api.openai.com/v1",
"huanke-gemini": "https://api.huandutech.com/v1",
"qianwen-video": "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation",
"doubao-video": "https://ark.cn-beijing.volces.com/api/v3",
"volcano-vision": "https://ark.cn-beijing.volces.com",
"volcano-video": "https://ark.cn-beijing.volces.com/api/v3",
"alibaba-image":
"https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
"ruxa-image": "https://api.ruxa.ai",
"fal-image": "https://fal.run",
"fal-video": "https://fal.run",
};
export function getProviderDefaultBaseUrl(type: AiProvider["type"]): string | undefined {
return PROVIDER_DEFAULT_BASE_URL[type];
}

View File

@ -10,6 +10,26 @@ config({
path: ".env.local",
});
const providerBaseUrlDefaults = [
{ type: "alibaba", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
{ type: "openai", baseUrl: "https://api.openai.com/v1" },
{ type: "huanke-gemini", baseUrl: "https://api.huandutech.com/v1" },
{
type: "qianwen-video",
baseUrl: "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation",
},
{ type: "doubao-video", baseUrl: "https://ark.cn-beijing.volces.com/api/v3" },
{ type: "volcano-vision", baseUrl: "https://ark.cn-beijing.volces.com" },
{ type: "volcano-video", baseUrl: "https://ark.cn-beijing.volces.com/api/v3" },
{
type: "alibaba-image",
baseUrl: "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
},
{ type: "ruxa-image", baseUrl: "https://api.ruxa.ai" },
{ type: "fal-image", baseUrl: "https://fal.run" },
{ type: "fal-video", baseUrl: "https://fal.run" },
] as const;
function assertMigrationJournalIsConsistent() {
const migrationsDir = join(process.cwd(), "lib/db/migrations");
const journalPath = join(migrationsDir, "meta", "_journal.json");
@ -70,6 +90,17 @@ const runMigrate = async () => {
'huanke-gemini', 'qianwen-video', 'doubao-video', 'volcano-vision', 'volcano-video', 'alibaba-image', 'ruxa-image', 'fal-image', 'fal-video', 'zhipu'
));
`);
for (const item of providerBaseUrlDefaults) {
await db.execute(sql`
UPDATE "AiProvider"
SET
"baseUrl" = ${item.baseUrl},
"updatedAt" = NOW()
WHERE
"type" = ${item.type}
AND ("baseUrl" IS NULL OR BTRIM("baseUrl") = '');
`);
}
const end = Date.now();
console.log("✅ Migrations completed in", end - start, "ms");