chore: harden dynamic provider logging

This commit is contained in:
Your Name 2026-05-27 10:55:58 +08:00
parent 303535b22a
commit a1d59e0db8
2 changed files with 93 additions and 83 deletions

View File

@ -12,6 +12,7 @@ import {
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";
@ -39,6 +40,7 @@ import { isBltcyVideoBaseUrl } from "./bltcy-video-models";
// 缓存已创建的 provider 实例
const providerCache = new Map<string, any>();
const logger = createLogger("dynamic-provider");
function isVolcanoMultimodalBaseUrl(baseUrl?: string | null) {
if (!baseUrl) return false;
@ -49,6 +51,20 @@ 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格式,但可能有细微差异
@ -80,7 +96,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
fetch: getOpenAICompatibleFetch(resolvedBaseUrl),
});
console.log(`[Provider] Created Alibaba DashScope provider: ${resolvedBaseUrl}`);
logProviderCreated("alibaba", resolvedBaseUrl);
break;
}
@ -92,7 +108,10 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl || "https://api.openai.com/v1",
fetch: getOpenAICompatibleFetch(resolvedBaseUrl),
});
console.log(`[Provider] Created OpenAI provider: ${resolvedBaseUrl || "https://api.openai.com/v1"}`);
logProviderCreated(
"openai",
resolvedBaseUrl || "https://api.openai.com/v1"
);
break;
}
@ -102,8 +121,9 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl || "https://open.bigmodel.cn/api/paas/v4",
fetch: getOpenAICompatibleFetch(resolvedBaseUrl),
});
console.log(
`[Provider] Created Zhipu Open Platform provider: ${resolvedBaseUrl || "https://open.bigmodel.cn/api/paas/v4"}`
logProviderCreated(
"zhipu",
resolvedBaseUrl || "https://open.bigmodel.cn/api/paas/v4"
);
break;
}
@ -116,7 +136,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created local model provider: ${resolvedBaseUrl}`);
logProviderCreated("local", resolvedBaseUrl);
return localProvider as any;
}
@ -127,7 +147,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Gemini image generation provider: ${resolvedBaseUrl}`);
logProviderCreated("gemini", resolvedBaseUrl);
return geminiProvider as any;
}
@ -136,14 +156,15 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Grsai GPT Image provider: ${resolvedBaseUrl}`);
logProviderCreated("grsai-gpt-image", resolvedBaseUrl);
return grsaiGptImageProvider as any;
}
case "grsai-video": {
console.warn(
`[Provider] Grsai Video provider is configured but its runtime adapter is not connected yet: ${resolvedBaseUrl}`
);
logger.warn("provider adapter unavailable", {
providerType: "grsai-video",
baseUrl: resolvedBaseUrl,
});
return null;
}
@ -154,7 +175,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Huanke Gemini image generation provider: ${resolvedBaseUrl}`);
logProviderCreated("huanke-gemini", resolvedBaseUrl);
return huankeGeminiProvider as any;
}
@ -165,7 +186,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Qianwen video generation provider: ${resolvedBaseUrl}`);
logProviderCreated("qianwen-video", resolvedBaseUrl);
return qianwenVideoProvider as any;
}
@ -175,8 +196,9 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl || "https://ark.cn-beijing.volces.com/api/v3",
fetch: getOpenAICompatibleFetch(resolvedBaseUrl),
});
console.log(
`[Provider] Created Doubao video provider: ${resolvedBaseUrl || "https://ark.cn-beijing.volces.com/api/v3"}`
logProviderCreated(
"doubao-video",
resolvedBaseUrl || "https://ark.cn-beijing.volces.com/api/v3"
);
break;
}
@ -187,7 +209,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Fal Video generation provider: ${resolvedBaseUrl}`);
logProviderCreated("fal-video", resolvedBaseUrl);
return falVideoProvider as any;
}
@ -197,7 +219,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Ruxa Video generation provider: ${resolvedBaseUrl}`);
logProviderCreated("ruxa-video", resolvedBaseUrl);
return ruxaVideoProvider as any;
}
@ -208,7 +230,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Volcano Vision image generation provider: ${resolvedBaseUrl}`);
logProviderCreated("volcano-vision", resolvedBaseUrl);
return volcanoVisionProvider as any;
}
@ -219,7 +241,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Alibaba Image generation provider: ${resolvedBaseUrl}`);
logProviderCreated("alibaba-image", resolvedBaseUrl);
return alibabaImageProvider as any;
}
@ -228,7 +250,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created BFL Image generation provider: ${resolvedBaseUrl}`);
logProviderCreated("bfl-image", resolvedBaseUrl);
return bflImageProvider as any;
}
@ -238,7 +260,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Ruxa Image generation provider: ${resolvedBaseUrl}`);
logProviderCreated("ruxa-image", resolvedBaseUrl);
return ruxaImageProvider as any;
}
@ -248,7 +270,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Fal Image generation provider: ${resolvedBaseUrl}`);
logProviderCreated("fal-image", resolvedBaseUrl);
return falImageProvider as any;
}
@ -257,7 +279,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created OpenAI Image generation provider: ${resolvedBaseUrl}`);
logProviderCreated("openai-image", resolvedBaseUrl);
return openAIImageProvider as any;
}
@ -266,7 +288,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created StepFun Image generation provider: ${resolvedBaseUrl}`);
logProviderCreated("stepfun-image", resolvedBaseUrl);
return stepfunImageProvider as any;
}
@ -275,7 +297,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Midjourney generation provider: ${resolvedBaseUrl}`);
logProviderCreated("midjourney", resolvedBaseUrl);
return midjourneyProvider as any;
}
@ -284,7 +306,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Runway provider: ${resolvedBaseUrl}`);
logProviderCreated("runway", resolvedBaseUrl);
return runwayProvider as any;
}
@ -293,7 +315,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Krea provider: ${resolvedBaseUrl}`);
logProviderCreated("krea", resolvedBaseUrl);
return kreaProvider as any;
}
@ -304,7 +326,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created BLTCY unified video provider: ${resolvedBaseUrl}`);
logProviderCreated("bltcy-video", resolvedBaseUrl);
return bltcyVideoProvider as any;
}
if (isVolcanoMultimodalBaseUrl(resolvedBaseUrl)) {
@ -312,9 +334,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl,
apiKey: provider.apiKey,
});
console.log(
`[Provider] Created Volcano Multimodal provider via Responses API: ${resolvedBaseUrl}`
);
logProviderCreated("volcano-multimodal", resolvedBaseUrl);
return volcanoMultimodalProvider as any;
}
// OpenAI 兼容的第三方 API(如 GPT-5 Nano、Claude via OpenAI proxy 等)
@ -325,9 +345,7 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
baseURL: resolvedBaseUrl || provider.baseUrl,
fetch: getOpenAICompatibleFetch(resolvedBaseUrl || provider.baseUrl),
});
console.log(
`[Provider] Created custom OpenAI-compatible provider: ${resolvedBaseUrl || provider.baseUrl}`
);
logProviderCreated("custom", resolvedBaseUrl || provider.baseUrl);
break;
}
}
@ -344,13 +362,13 @@ export async function getDynamicModel(
try {
const model = await getAiModelById({ id: modelId });
if (!model || !model.isEnabled) {
console.warn(`[getDynamicModel] Model ${modelId} not found or disabled`);
logger.warn("model unavailable", { modelId });
return null;
}
const provider = await getAiProviderById({ id: model.providerId });
if (!provider || !provider.isEnabled) {
console.warn(`[getDynamicModel] Provider for model ${modelId} not found or disabled`);
logger.warn("provider unavailable", { modelId, providerId: model.providerId });
return null;
}
@ -363,14 +381,21 @@ export async function getDynamicModel(
});
if (!providerInstance) {
console.error(`[getDynamicModel] Failed to create provider instance for ${provider.type}`);
logger.error("provider instance creation failed", {
providerType: provider.type,
});
return null;
}
const resolvedModelId = String(overrideModelId || "").trim() || model.modelId;
// 创建基础模型
console.log(`[getDynamicModel] Creating model: ${resolvedModelId} (${model.displayName}) using provider: ${provider.name} (${provider.type})`);
logger.debug("model creation started", {
modelId,
resolvedModelId,
providerId: provider.id,
providerType: provider.type,
});
// 根据 provider 类型选择正确的方法和模型ID
// customProvider (gemini, local) 使用 .languageModel()
@ -379,105 +404,84 @@ export async function getDynamicModel(
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 相同的调用方式)
console.log(`[getDynamicModel] Getting ${imageProviderLabel} chat model...`); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log(`[getDynamicModel] ${imageProviderLabel} model retrieved:`, baseModel ? "success" : "failed"); // 调试日志
console.log(`[getDynamicModel] ${imageProviderLabel} model type:`, typeof baseModel); // 调试
console.log(`[getDynamicModel] ${imageProviderLabel} model keys:`, baseModel ? Object.keys(baseModel).join(', ') : 'N/A'); // 调试
logChatModelResult(imageProviderLabel, baseModel);
} else if (provider.type === "local") {
// 本地模型使用 chat() 方法
console.log("[getDynamicModel] Getting local chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Local model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("local", baseModel);
} else if (provider.type === "qianwen-video") {
// 千问视频生成使用 chat() 方法
console.log("[getDynamicModel] Getting Qianwen Video chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Qianwen Video model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("qianwen-video", baseModel);
} else if (provider.type === "fal-video") {
// FAL 视频生成使用 chat() 方法
console.log("[getDynamicModel] Getting Fal Video chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Fal Video model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("fal-video", baseModel);
} else if (provider.type === "ruxa-video") {
// RUXA 视频生成使用 chat() 方法
console.log("[getDynamicModel] Getting Ruxa Video chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Ruxa Video model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("ruxa-video", baseModel);
} else if (provider.type === "volcano-vision") {
// 火山引擎视觉生成使用 chat() 方法
console.log("[getDynamicModel] Getting Volcano Vision chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Volcano Vision model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("volcano-vision", baseModel);
} else if (provider.type === "alibaba-image") {
// 阿里云千问图片生成使用 chat() 方法
console.log("[getDynamicModel] Getting Alibaba Image chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Alibaba Image model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("alibaba-image", baseModel);
} else if (provider.type === "bfl-image") {
console.log("[getDynamicModel] Getting BFL Image chat model...");
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] BFL Image model retrieved:", baseModel ? "success" : "failed");
logChatModelResult("bfl-image", baseModel);
} else if (provider.type === "ruxa-image") {
// RUXA 图片编辑生成使用 chat() 方法
console.log("[getDynamicModel] Getting Ruxa Image chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Ruxa Image model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("ruxa-image", baseModel);
} else if (provider.type === "fal-image") {
// FAL 图片编辑生成使用 chat() 方法
console.log("[getDynamicModel] Getting Fal Image chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Fal Image model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("fal-image", baseModel);
} else if (provider.type === "openai-image") {
// OpenAI Images 兼容图片生成/编辑使用 chat() 方法
console.log("[getDynamicModel] Getting OpenAI Image chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] OpenAI Image model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("openai-image", baseModel);
} else if (provider.type === "stepfun-image") {
console.log("[getDynamicModel] Getting StepFun Image chat model...");
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] StepFun Image model retrieved:", baseModel ? "success" : "failed");
logChatModelResult("stepfun-image", baseModel);
} else if (provider.type === "midjourney") {
console.log("[getDynamicModel] Getting Midjourney chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Midjourney model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("midjourney", baseModel);
} else if (provider.type === "runway") {
console.log("[getDynamicModel] Getting Runway chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Runway model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("runway", baseModel);
} else if (provider.type === "krea") {
console.log("[getDynamicModel] Getting Krea chat model...");
baseModel = providerInstance.chat(resolvedModelId);
console.log("[getDynamicModel] Krea model retrieved:", baseModel ? "success" : "failed");
logChatModelResult("krea", baseModel);
} else if (provider.type === "huanke-gemini") {
// 环科 Gemini 原生格式使用 chat() 方法
console.log("[getDynamicModel] Getting Huanke Gemini chat model..."); // 调试日志
baseModel = providerInstance.chat(resolvedModelId);
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'); // 调试
logChatModelResult("huanke-gemini", baseModel);
} else {
// OpenAI-compatible providers
console.log("[getDynamicModel] Getting OpenAI-compatible model..."); // 调试日志
baseModel = (providerInstance as any).chat(resolvedModelId);
console.log("[getDynamicModel] OpenAI model retrieved:", baseModel ? "success" : "failed"); // 调试日志
logChatModelResult("openai-compatible", baseModel);
}
// 如果模型名称包含特定关键词,可以添加推理中间件
// 例如,为推理模型添加思考过程提取
if (model.displayName.toLowerCase().includes("reasoning") ||
model.displayName.toLowerCase().includes("推理")) {
console.log("[getDynamicModel] Applying reasoning middleware..."); // 调试日志
logger.debug("reasoning middleware applied", { modelId, resolvedModelId });
return wrapLanguageModel({
model: baseModel,
middleware: extractReasoningMiddleware({ tagName: "think" }),
});
}
console.log("[getDynamicModel] Returning base model"); // 调试日志
logger.debug("base model returned", { modelId, resolvedModelId });
return baseModel;
} catch (error) {
console.error("[getDynamicModel] Failed to get dynamic model:", error);
logger.error("dynamic model resolution failed", error, { modelId });
return null;
}
}
@ -506,7 +510,7 @@ export async function getAvailableModels() {
providerType: provider.type,
}));
} catch (error) {
console.error("Failed to get available models:", error);
logger.error("available models load failed", error);
return [];
}
}
@ -527,7 +531,12 @@ export async function getSystemModel(
if (defaultModelData) {
const { model, provider } = defaultModelData;
console.log(`[getSystemModel] Using default model for ${purpose}: ${model.displayName} (${provider.name})`);
logger.debug("system model default selected", {
purpose,
modelId: model.id,
providerId: provider.id,
providerType: provider.type,
});
const providerInstance = getOrCreateProvider({
id: provider.id,
@ -542,7 +551,7 @@ export async function getSystemModel(
}
// 如果没有找到数据库配置,回退到环境变量配置
console.warn(`[getSystemModel] No default model found in database for ${purpose}, falling back to env config`);
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,
@ -551,10 +560,10 @@ export async function getSystemModel(
return fallbackProvider.chat("qwen-turbo");
}
console.error(`[getSystemModel] No model configuration found for ${purpose}`);
logger.error("system model configuration missing", { purpose });
return null;
} catch (error) {
console.error(`[getSystemModel] Failed to get system model for ${purpose}:`, error);
logger.error("system model resolution failed", error, { purpose });
return null;
}
}

View File

@ -24,6 +24,7 @@ const files = [
"lib/ai/providers/gemini-image.ts",
"lib/ai/providers/volcano-vision.ts",
"lib/ai/providers/qianwen-video.ts",
"lib/ai/dynamic-provider.ts",
"services/canvas/index.ts",
"components/image-annotator.tsx",
"app/api/canvas/auto-title/route.ts",