ai-chatbot/lib/ai/dynamic-provider.ts

642 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
import { createOpenAI } from "@ai-sdk/openai";
import {
extractReasoningMiddleware,
wrapLanguageModel,
type LanguageModel,
} from "ai";
import {
getEnabledAiModelsWithProvider,
getAiProviderById,
getAiModelById,
} from "@/lib/db/queries";
import type { AiProvider } from "@/lib/db/schema";
import { createLogger } from "@/lib/observability/logger";
import { createLocalQwen } from "./providers/local-qwen";
import { createGeminiImage } from "./providers/gemini-image";
import { createHuankeGemini } from "./providers/huanke-gemini";
import { createQianwenVideo } from "./providers/qianwen-video";
import { createVolcanoVision } from "./providers/volcano-vision";
import { createAlibabaImage } from "./providers/alibaba-image";
import { createBflImage } from "./providers/bfl-image";
import { createRuxaImage } from "./providers/ruxa-image";
import { createFalImage } from "./providers/fal-image";
import { createSnapgenImage } from "./providers/snapgen-image";
import { createOpenAIImage } from "./providers/openai-image";
import { createBananaImage } from "./providers/bananaimage";
import { createVisionary } from "./providers/visionary";
import { createAtlasCloud } from "./providers/atlascloud";
import { createVip98k } from "./providers/vip98k";
import { createStepfunImage } from "./providers/stepfun-image";
import { createMidjourney } from "./providers/midjourney";
import { createFalVideo } from "./providers/fal-video";
import { createRuxaVideo } from "./providers/ruxa-video";
import { createRunway } from "./providers/runway";
import { createKrea } from "./providers/krea";
import { createVolcanoMultimodal } from "./providers/volcano-multimodal";
import { createBltcyVideo } from "./providers/bltcy-video";
import { getProviderDefaultBaseUrl } from "./provider-defaults";
import {
createMoonshotKimiFetch,
isMoonshotOfficialBaseUrl,
} from "./detail-agent-kimi-profile";
import { isBltcyVideoBaseUrl } from "./bltcy-video-models";
import { resolveModelRuntimeId } from "./model-runtime";
// 缓存已创建的 provider 实例
const providerCache = new Map<string, any>();
const logger = createLogger("dynamic-provider");
function isVolcanoMultimodalBaseUrl(baseUrl?: string | null) {
if (!baseUrl) return false;
return /ark\.cn-beijing\.volces\.com/i.test(baseUrl);
}
function getOpenAICompatibleFetch(baseUrl?: string | null) {
return isMoonshotOfficialBaseUrl(baseUrl) ? createMoonshotKimiFetch() : undefined;
}
function logProviderCreated(providerType: string, baseUrl?: string | null) {
logger.debug("provider created", { providerType, baseUrl });
}
function logChatModelResult(providerType: string, baseModel: unknown) {
logger.debug("chat model resolved", {
providerType,
success: Boolean(baseModel),
modelType: typeof baseModel,
modelKeyCount:
baseModel && typeof baseModel === "object" ? Object.keys(baseModel).length : 0,
});
}
/**
* 根据提供商类型创建对应的 AI Provider 实例
* 不同供应商使用OpenAI兼容的API格式,但可能有细微差异
*
* 支持的供应商类型:
* - alibaba: 阿里云 DashScope (通义千问等)
* - openai: 官方 OpenAI API
* - custom: OpenAI兼容的第三方API (GPT-5 Nano、其他兼容服务等)
*/
function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl" | "apiKey">) {
const resolvedBaseUrl =
provider.baseUrl?.trim() || getProviderDefaultBaseUrl(provider.type) || "";
const cacheKey = `${provider.id}-${resolvedBaseUrl}`;
if (providerCache.has(cacheKey)) {
return providerCache.get(cacheKey);
}
let providerInstance: ReturnType<typeof createOpenAI>;
// 根据供应商类型创建不同的配置
// 虽然都使用 createOpenAI,但通过 baseURL 和 apiKey 的不同来区分不同供应商
switch (provider.type) {
case "alibaba": {
// 阿里云 DashScope API 配置
// 阿里云提供 OpenAI 兼容的接口:https://dashscope.aliyuncs.com/compatible-mode/v1
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: resolvedBaseUrl,
fetch: getOpenAICompatibleFetch(resolvedBaseUrl),
});
logProviderCreated("alibaba", resolvedBaseUrl);
break;
}
case "openai": {
// 标准 OpenAI API 配置
// 官方 API 地址:https://api.openai.com/v1
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: resolvedBaseUrl || "https://api.openai.com/v1",
fetch: getOpenAICompatibleFetch(resolvedBaseUrl),
});
logProviderCreated(
"openai",
resolvedBaseUrl || "https://api.openai.com/v1"
);
break;
}
case "zhipu": {
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: resolvedBaseUrl || "https://open.bigmodel.cn/api/paas/v4",
fetch: getOpenAICompatibleFetch(resolvedBaseUrl),
});
logProviderCreated(
"zhipu",
resolvedBaseUrl || "https://open.bigmodel.cn/api/paas/v4"
);
break;
}
case "local": {
// 本地部署的模型(如千问2.5-VL)
// 使用自定义的本地模型接口,不兼容 OpenAI 格式
// 本地模型不使用缓存,因为类型不兼容 OpenAIProvider
const localProvider = createLocalQwen({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("local", resolvedBaseUrl);
return localProvider as any;
}
case "gemini": {
// Gemini 图像生成中转服务
// 使用特殊的响应格式处理(Markdown + inline_data)
const geminiProvider = createGeminiImage({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("gemini", resolvedBaseUrl);
return geminiProvider as any;
}
case "grsai-gpt-image": {
const grsaiGptImageProvider = createGeminiImage({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("grsai-gpt-image", resolvedBaseUrl);
return grsaiGptImageProvider as any;
}
case "grsai-video": {
logger.warn("provider adapter unavailable", {
providerType: "grsai-video",
baseUrl: resolvedBaseUrl,
});
return null;
}
case "huanke-gemini": {
// 环科 Gemini 原生格式 API
// 使用 /v1beta/models/{model}:generateContent 端点
const huankeGeminiProvider = createHuankeGemini({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("huanke-gemini", resolvedBaseUrl);
return huankeGeminiProvider as any;
}
case "qianwen-video": {
// 千问视频生成服务(通义万相 I2V)
// 支持图片转视频功能,异步任务轮询
const qianwenVideoProvider = createQianwenVideo({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("qianwen-video", resolvedBaseUrl);
return qianwenVideoProvider as any;
}
case "doubao-video": {
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: resolvedBaseUrl || "https://ark.cn-beijing.volces.com/api/v3",
fetch: getOpenAICompatibleFetch(resolvedBaseUrl),
});
logProviderCreated(
"doubao-video",
resolvedBaseUrl || "https://ark.cn-beijing.volces.com/api/v3"
);
break;
}
case "fal-video": {
// FAL 视频生成服务(Kling O3 等)
const falVideoProvider = createFalVideo({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("fal-video", resolvedBaseUrl);
return falVideoProvider as any;
}
case "ruxa-video": {
// RUXA 视频生成服务(Veo 3.1)
const ruxaVideoProvider = createRuxaVideo({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("ruxa-video", resolvedBaseUrl);
return ruxaVideoProvider as any;
}
case "volcano-vision": {
// 火山引擎视觉图片生成服务(Seedream)
// 支持文生图、图生图、多图融合等功能
const volcanoVisionProvider = createVolcanoVision({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("volcano-vision", resolvedBaseUrl);
return volcanoVisionProvider as any;
}
case "alibaba-image": {
// 阿里云千问图片生成/编辑服务
// 支持 qwen-image-edit-plus(1-6张输出)和 qwen-image-edit(1张输出)
const alibabaImageProvider = createAlibabaImage({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("alibaba-image", resolvedBaseUrl);
return alibabaImageProvider as any;
}
case "bfl-image": {
const bflImageProvider = createBflImage({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("bfl-image", resolvedBaseUrl);
return bflImageProvider as any;
}
case "ruxa-image": {
// RUXA 图片编辑服务
const ruxaImageProvider = createRuxaImage({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("ruxa-image", resolvedBaseUrl);
return ruxaImageProvider as any;
}
case "fal-image": {
// FAL 图片编辑服务(Nano Banana Pro)
const falImageProvider = createFalImage({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("fal-image", resolvedBaseUrl);
return falImageProvider as any;
}
case "snapgen-image": {
const snapgenImageProvider = createSnapgenImage({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("snapgen-image", resolvedBaseUrl);
return snapgenImageProvider as any;
}
case "openai-image": {
const openAIImageProvider = createOpenAIImage({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("openai-image", resolvedBaseUrl);
return openAIImageProvider as any;
}
case "bananaimage": {
const bananaImageProvider = createBananaImage({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("bananaimage", resolvedBaseUrl);
return bananaImageProvider as any;
}
case "visionary": {
const visionaryProvider = createVisionary({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("visionary", resolvedBaseUrl);
return visionaryProvider as any;
}
case "atlascloud": {
const atlasCloudProvider = createAtlasCloud({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("atlascloud", resolvedBaseUrl);
return atlasCloudProvider as any;
}
case "vip98k": {
const vip98kProvider = createVip98k({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("vip98k", resolvedBaseUrl);
return vip98kProvider as any;
}
case "stepfun-image": {
const stepfunImageProvider = createStepfunImage({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("stepfun-image", resolvedBaseUrl);
return stepfunImageProvider as any;
}
case "midjourney": {
const midjourneyProvider = createMidjourney({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("midjourney", resolvedBaseUrl);
return midjourneyProvider as any;
}
case "runway": {
const runwayProvider = createRunway({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("runway", resolvedBaseUrl);
return runwayProvider as any;
}
case "krea": {
const kreaProvider = createKrea({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("krea", resolvedBaseUrl);
return kreaProvider as any;
}
case "custom":
default: {
if (isBltcyVideoBaseUrl(resolvedBaseUrl)) {
const bltcyVideoProvider = createBltcyVideo({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("bltcy-video", resolvedBaseUrl);
return bltcyVideoProvider as any;
}
if (isVolcanoMultimodalBaseUrl(resolvedBaseUrl)) {
const volcanoMultimodalProvider = createVolcanoMultimodal({
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
logProviderCreated("volcano-multimodal", resolvedBaseUrl);
return volcanoMultimodalProvider as any;
}
// OpenAI 兼容的第三方 API(如 GPT-5 Nano、Claude via OpenAI proxy 等)
// 这些服务提供与 OpenAI API 完全兼容的接口
// 请求格式遵循 OpenAI Chat Completions 标准:/v1/chat/completions
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: resolvedBaseUrl || provider.baseUrl,
fetch: getOpenAICompatibleFetch(resolvedBaseUrl || provider.baseUrl),
});
logProviderCreated("custom", resolvedBaseUrl || provider.baseUrl);
break;
}
}
providerCache.set(cacheKey, providerInstance);
return providerInstance;
}
// 获取动态配置的语言模型
export async function getDynamicModel(
modelId: string,
overrideModelId?: string | null
): Promise<LanguageModel | null> {
try {
const model = await getAiModelById({ id: modelId });
if (!model || !model.isEnabled) {
logger.warn("model unavailable", { modelId });
return null;
}
const provider = await getAiProviderById({ id: model.providerId });
if (!provider || !provider.isEnabled) {
logger.warn("provider unavailable", { modelId, providerId: model.providerId });
return null;
}
// 根据供应商类型创建对应的 provider 实例
const providerInstance = getOrCreateProvider({
id: provider.id,
type: provider.type,
baseUrl: provider.baseUrl,
apiKey: provider.apiKey,
});
if (!providerInstance) {
logger.error("provider instance creation failed", {
providerType: provider.type,
});
return null;
}
const resolvedModelId = resolveModelRuntimeId(model, {
overrideModelId,
providerType: provider.type,
});
// 创建基础模型
logger.debug("model creation started", {
modelId,
resolvedModelId,
providerId: provider.id,
providerType: provider.type,
});
// 根据 provider 类型选择正确的方法和模型ID
// customProvider (gemini, local) 使用 .languageModel()
// OpenAI-compatible 使用 .chat()
let baseModel;
if (provider.type === "gemini" || provider.type === "grsai-gpt-image") {
const imageProviderLabel = provider.type === "grsai-gpt-image" ? "Grsai GPT Image" : "Gemini image";
// 图片生成 provider 使用 chat() 方法(与 local-qwen 相同的调用方式)
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult(imageProviderLabel, baseModel);
} else if (provider.type === "local") {
// 本地模型使用 chat() 方法
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("local", baseModel);
} else if (provider.type === "qianwen-video") {
// 千问视频生成使用 chat() 方法
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("qianwen-video", baseModel);
} else if (provider.type === "fal-video") {
// FAL 视频生成使用 chat() 方法
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("fal-video", baseModel);
} else if (provider.type === "ruxa-video") {
// RUXA 视频生成使用 chat() 方法
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("ruxa-video", baseModel);
} else if (provider.type === "volcano-vision") {
// 火山引擎视觉生成使用 chat() 方法
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("volcano-vision", baseModel);
} else if (provider.type === "alibaba-image") {
// 阿里云千问图片生成使用 chat() 方法
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("alibaba-image", baseModel);
} else if (provider.type === "bfl-image") {
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("bfl-image", baseModel);
} else if (provider.type === "ruxa-image") {
// RUXA 图片编辑生成使用 chat() 方法
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("ruxa-image", baseModel);
} else if (provider.type === "fal-image") {
// FAL 图片编辑生成使用 chat() 方法
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("fal-image", baseModel);
} else if (provider.type === "snapgen-image") {
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("snapgen-image", baseModel);
} else if (provider.type === "openai-image") {
// OpenAI Images 兼容图片生成/编辑使用 chat() 方法
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("openai-image", baseModel);
} else if (provider.type === "bananaimage") {
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("bananaimage", baseModel);
} else if (provider.type === "visionary") {
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("visionary", baseModel);
} else if (provider.type === "atlascloud") {
baseModel = providerInstance.chat(resolvedModelId, model.imageGenConfig || null);
logChatModelResult("atlascloud", baseModel);
} else if (provider.type === "vip98k") {
baseModel = providerInstance.chat(resolvedModelId, {
capabilities: model.capabilities || null,
imageGenConfig: model.imageGenConfig || null,
});
logChatModelResult("vip98k", baseModel);
} else if (provider.type === "stepfun-image") {
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("stepfun-image", baseModel);
} else if (provider.type === "midjourney") {
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("midjourney", baseModel);
} else if (provider.type === "runway") {
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("runway", baseModel);
} else if (provider.type === "krea") {
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("krea", baseModel);
} else if (provider.type === "huanke-gemini") {
// 环科 Gemini 原生格式使用 chat() 方法
baseModel = providerInstance.chat(resolvedModelId);
logChatModelResult("huanke-gemini", baseModel);
} else {
// OpenAI-compatible providers
baseModel = (providerInstance as any).chat(resolvedModelId);
logChatModelResult("openai-compatible", baseModel);
}
// 如果模型名称包含特定关键词,可以添加推理中间件
// 例如,为推理模型添加思考过程提取
if (model.displayName.toLowerCase().includes("reasoning") ||
model.displayName.toLowerCase().includes("推理")) {
logger.debug("reasoning middleware applied", { modelId, resolvedModelId });
return wrapLanguageModel({
model: baseModel,
middleware: extractReasoningMiddleware({ tagName: "think" }),
});
}
logger.debug("base model returned", { modelId, resolvedModelId });
return baseModel;
} catch (error) {
logger.error("dynamic model resolution failed", error, { modelId });
return null;
}
}
// 通过模型的数据库 ID 获取语言模型实例
export async function getLanguageModelById(
modelId: string,
overrideModelId?: string | null
): Promise<LanguageModel | null> {
return getDynamicModel(modelId, overrideModelId);
}
// 获取所有可用的模型列表(用于前端展示)
export async function getAvailableModels() {
try {
const modelsWithProvider = await getEnabledAiModelsWithProvider();
return modelsWithProvider.map(({ model, provider }) => ({
id: model.id,
modelId: model.modelId,
displayName: model.displayName,
description: model.description,
supportsVision: model.supportsVision,
supportsImageGen: model.supportsImageGen,
isDefault: model.isDefault,
providerName: provider.name,
providerType: provider.type,
}));
} catch (error) {
logger.error("available models load failed", error);
return [];
}
}
// 获取系统模型(标题生成、文档生成等)
// 这些模型使用数据库中标记为默认的模型,如果没有则回退到环境变量配置
export async function getSystemModel(
purpose: "title" | "artifact"
): Promise<LanguageModel | null> {
try {
// 首先尝试从数据库获取默认模型
const modelsWithProvider = await getEnabledAiModelsWithProvider();
// 找到默认模型
const defaultModelData = modelsWithProvider.find(
({ model }) => model.isDefault
);
if (defaultModelData) {
const { model, provider } = defaultModelData;
logger.debug("system model default selected", {
purpose,
modelId: model.id,
providerId: provider.id,
providerType: provider.type,
});
const providerInstance = getOrCreateProvider({
id: provider.id,
type: provider.type,
baseUrl: provider.baseUrl,
apiKey: provider.apiKey,
});
if (providerInstance) {
return providerInstance.chat(model.modelId);
}
}
// 如果没有找到数据库配置,回退到环境变量配置
logger.warn("system model default missing, using env fallback", { purpose });
if (process.env.DASHSCOPE_API_KEY && process.env.DASHSCOPE_BASE_URL) {
const fallbackProvider = createOpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
baseURL: process.env.DASHSCOPE_BASE_URL,
});
return fallbackProvider.chat("qwen-turbo");
}
logger.error("system model configuration missing", { purpose });
return null;
} catch (error) {
logger.error("system model resolution failed", error, { purpose });
return null;
}
}