fix(provider): auto backfill default base urls on migration
This commit is contained in:
parent
bdf980044a
commit
b7de92ec4e
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
21
lib/ai/provider-defaults.ts
Normal file
21
lib/ai/provider-defaults.ts
Normal 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];
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user