ai-chatbot/lib/ai/dynamic-provider.ts
WuYongJie 00cef71be7 暂停欠费生图线路并修正 98K 文生图尺寸传递
## Summary

生图线路确认欠费后立即停止新请求,跳过同一上游账户的其他模型,保留已生成结果和已提交任务。欠费状态须经管理员恢复,不再定时自动解封,也不会被旧任务的成功回执清除。旧模型选择仅使用明确配置且尺寸、比例和参考图数量兼容的备用模型;固定模型模式仍要求用户自行选择。

98K 文生图优先使用其公开声明的任务接口,原生传递分辨率和比例,清除文生图请求中的示例参考图片。

## Scope

- [x] API / backend logic
- [x] Settings / admin configuration

## Required Verification

验证普通 Pro、Banana 2、两个 Plus 和 GPT Image 2 共 10 个真实文生图及参考图编辑用例,均获得可下载的 2048×2048 图片。625 项 helper 回归通过,1 项跳过;TypeScript、日志/品牌/CI/环境/凭据/元数据/测试/产物检查通过。全仓 Biome 仍有历史格式和 lint 诊断(1005 项)。构建机执行生产构建及路由烟测;生产切换前核对任务与请求,保留旧构建和模型配置回滚记录。

## Release Readiness

仅修改本次生图运行逻辑与测试,凭据和验证产物保存在仓库外或忽略目录中。内部生成接口的权限与访问方式保持兼容。
2026-11-03 19:20:18 +08:00

649 lines
23 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 { isImageModelBalanceBlocked } from "@/lib/ai/runtime/image-balance";
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";
import { createDeepSeekCompatibleFetch, isDeepSeekOfficialUrl } from "./deepseek-compatible-fetch";
// 缓存已创建的 provider 实例
const providerCache = new Map<string, {
type: string; baseUrl: string; apiKey: string; instance: 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) {
if (isDeepSeekOfficialUrl(baseUrl)) return createDeepSeekCompatibleFetch();
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 cached = providerCache.get(provider.id);
if (cached && cached.type === provider.type && cached.baseUrl === resolvedBaseUrl &&
cached.apiKey === provider.apiKey) {
return cached.instance;
}
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(provider.id, {
type: provider.type, baseUrl: resolvedBaseUrl, apiKey: provider.apiKey, instance: 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 || (model.supportsImageGen && isImageModelBalanceBlocked(model))) {
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;
}
}