lovart3/lib/ai/dynamic-provider.ts

326 lines
13 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 { 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";
// 缓存已创建的 provider 实例
const providerCache = new Map<string, ReturnType<typeof createOpenAI>>();
/**
* 根据提供商类型创建对应的 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 cacheKey = `${provider.id}-${provider.baseUrl}`;
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: provider.baseUrl,
});
console.log(`[Provider] Created Alibaba DashScope provider: ${provider.baseUrl}`);
break;
}
case "openai": {
// 标准 OpenAI API 配置
// 官方 API 地址:https://api.openai.com/v1
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: provider.baseUrl || "https://api.openai.com/v1",
});
console.log(`[Provider] Created OpenAI provider: ${provider.baseUrl || "https://api.openai.com/v1"}`);
break;
}
case "local": {
// 本地部署的模型(如千问2.5-VL)
// 使用自定义的本地模型接口,不兼容 OpenAI 格式
// 本地模型不使用缓存,因为类型不兼容 OpenAIProvider
const localProvider = createLocalQwen({
baseURL: provider.baseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created local model provider: ${provider.baseUrl}`);
return localProvider as any;
}
case "gemini": {
// Gemini 图像生成中转服务
// 使用特殊的响应格式处理(Markdown + inline_data)
const geminiProvider = createGeminiImage({
baseURL: provider.baseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Gemini image generation provider: ${provider.baseUrl}`);
return geminiProvider as any;
}
case "huanke-gemini": {
// 环科 Gemini 原生格式 API
// 使用 /v1beta/models/{model}:generateContent 端点
const huankeGeminiProvider = createHuankeGemini({
baseURL: provider.baseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Huanke Gemini image generation provider: ${provider.baseUrl}`);
return huankeGeminiProvider as any;
}
case "qianwen-video": {
// 千问视频生成服务(通义万相 I2V)
// 支持图片转视频功能,异步任务轮询
const qianwenVideoProvider = createQianwenVideo({
baseURL: provider.baseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Qianwen video generation provider: ${provider.baseUrl}`);
return qianwenVideoProvider as any;
}
case "volcano-vision": {
// 火山引擎视觉图片生成服务(Seedream)
// 支持文生图、图生图、多图融合等功能
const volcanoVisionProvider = createVolcanoVision({
baseURL: provider.baseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Volcano Vision image generation provider: ${provider.baseUrl}`);
return volcanoVisionProvider as any;
}
case "alibaba-image": {
// 阿里云千问图片生成/编辑服务
// 支持 qwen-image-edit-plus(1-6张输出)和 qwen-image-edit(1张输出)
const alibabaImageProvider = createAlibabaImage({
baseURL: provider.baseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Alibaba Image generation provider: ${provider.baseUrl}`);
return alibabaImageProvider as any;
}
case "custom":
default: {
// OpenAI 兼容的第三方 API(如 GPT-5 Nano、Claude via OpenAI proxy 等)
// 这些服务提供与 OpenAI API 完全兼容的接口
// 请求格式遵循 OpenAI Chat Completions 标准:/v1/chat/completions
providerInstance = createOpenAI({
apiKey: provider.apiKey,
baseURL: provider.baseUrl,
});
console.log(`[Provider] Created custom OpenAI-compatible provider: ${provider.baseUrl}`);
break;
}
}
providerCache.set(cacheKey, providerInstance);
return providerInstance;
}
// 获取动态配置的语言模型
export async function getDynamicModel(
modelId: string
): Promise<LanguageModel | null> {
try {
const model = await getAiModelById({ id: modelId });
if (!model || !model.isEnabled) {
console.warn(`[getDynamicModel] Model ${modelId} not found or disabled`);
return null;
}
const provider = await getAiProviderById({ id: model.providerId });
if (!provider || !provider.isEnabled) {
console.warn(`[getDynamicModel] Provider for model ${modelId} not found or disabled`);
return null;
}
// 根据供应商类型创建对应的 provider 实例
const providerInstance = getOrCreateProvider({
id: provider.id,
type: provider.type,
baseUrl: provider.baseUrl,
apiKey: provider.apiKey,
});
if (!providerInstance) {
console.error(`[getDynamicModel] Failed to create provider instance for ${provider.type}`);
return null;
}
// 创建基础模型
console.log(`[getDynamicModel] Creating model: ${model.modelId} (${model.displayName}) using provider: ${provider.name} (${provider.type})`);
// 根据 provider 类型选择正确的方法和模型ID
// customProvider (gemini, local) 使用 .languageModel()
// OpenAI-compatible 使用 .chat()
let baseModel;
if (provider.type === "gemini") {
// Gemini 图片生成使用 chat() 方法(与 local-qwen 相同的调用方式)
console.log("[getDynamicModel] Getting Gemini chat model..."); // 调试日志
baseModel = providerInstance.chat(model.modelId);
console.log("[getDynamicModel] Gemini model retrieved:", baseModel ? "success" : "failed"); // 调试日志
console.log("[getDynamicModel] Gemini model type:", typeof baseModel); // 调试
console.log("[getDynamicModel] Gemini model keys:", baseModel ? Object.keys(baseModel).join(', ') : 'N/A'); // 调试
} else if (provider.type === "local") {
// 本地模型使用 chat() 方法
console.log("[getDynamicModel] Getting local chat model..."); // 调试日志
baseModel = providerInstance.chat(model.modelId);
console.log("[getDynamicModel] Local model retrieved:", baseModel ? "success" : "failed"); // 调试日志
} else if (provider.type === "qianwen-video") {
// 千问视频生成使用 chat() 方法
console.log("[getDynamicModel] Getting Qianwen Video chat model..."); // 调试日志
baseModel = providerInstance.chat(model.modelId);
console.log("[getDynamicModel] Qianwen Video model retrieved:", baseModel ? "success" : "failed"); // 调试日志
} else if (provider.type === "volcano-vision") {
// 火山引擎视觉生成使用 chat() 方法
console.log("[getDynamicModel] Getting Volcano Vision chat model..."); // 调试日志
baseModel = providerInstance.chat(model.modelId);
console.log("[getDynamicModel] Volcano Vision model retrieved:", baseModel ? "success" : "failed"); // 调试日志
} else if (provider.type === "alibaba-image") {
// 阿里云千问图片生成使用 chat() 方法
console.log("[getDynamicModel] Getting Alibaba Image chat model..."); // 调试日志
baseModel = providerInstance.chat(model.modelId);
console.log("[getDynamicModel] Alibaba Image model retrieved:", baseModel ? "success" : "failed"); // 调试日志
} else if (provider.type === "huanke-gemini") {
// 环科 Gemini 原生格式使用 chat() 方法
console.log("[getDynamicModel] Getting Huanke Gemini chat model..."); // 调试日志
baseModel = providerInstance.chat(model.modelId);
console.log("[getDynamicModel] Huanke Gemini model retrieved:", baseModel ? "success" : "failed"); // 调试日志
console.log("[getDynamicModel] Huanke Gemini model type:", typeof baseModel); // 调试
console.log("[getDynamicModel] Huanke Gemini model keys:", baseModel ? Object.keys(baseModel).join(', ') : 'N/A'); // 调试
} else {
// OpenAI-compatible providers
console.log("[getDynamicModel] Getting OpenAI-compatible model..."); // 调试日志
baseModel = (providerInstance as any).chat(model.modelId);
console.log("[getDynamicModel] OpenAI model retrieved:", baseModel ? "success" : "failed"); // 调试日志
}
// 如果模型名称包含特定关键词,可以添加推理中间件
// 例如,为推理模型添加思考过程提取
if (model.displayName.toLowerCase().includes("reasoning") ||
model.displayName.toLowerCase().includes("推理")) {
console.log("[getDynamicModel] Applying reasoning middleware..."); // 调试日志
return wrapLanguageModel({
model: baseModel,
middleware: extractReasoningMiddleware({ tagName: "think" }),
});
}
console.log("[getDynamicModel] Returning base model"); // 调试日志
return baseModel;
} catch (error) {
console.error("[getDynamicModel] Failed to get dynamic model:", error);
return null;
}
}
// 通过模型的数据库 ID 获取语言模型实例
export async function getLanguageModelById(
modelId: string
): Promise<LanguageModel | null> {
return getDynamicModel(modelId);
}
// 获取所有可用的模型列表(用于前端展示)
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) {
console.error("Failed to get available models:", 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;
console.log(`[getSystemModel] Using default model for ${purpose}: ${model.displayName} (${provider.name})`);
const providerInstance = getOrCreateProvider({
id: provider.id,
type: provider.type,
baseUrl: provider.baseUrl,
apiKey: provider.apiKey,
});
if (providerInstance) {
return providerInstance.chat(model.modelId);
}
}
// 如果没有找到数据库配置,回退到环境变量配置
console.warn(`[getSystemModel] No default model found in database for ${purpose}, falling back to env config`);
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");
}
console.error(`[getSystemModel] No model configuration found for ${purpose}`);
return null;
} catch (error) {
console.error(`[getSystemModel] Failed to get system model for ${purpose}:`, error);
return null;
}
}