ai-chatbot/lib/product-planning/model-runtime.ts
Your Name cd44deb8c4
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add dedicated model for product planning
2026-03-26 17:42:38 +08:00

180 lines
4.6 KiB
TypeScript
Raw Permalink 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 type { LanguageModel } from "ai";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
import {
isImageGenerationModelDescriptor,
isVideoModelDescriptor,
} from "@/lib/ai/model-classification";
import { isAiModelMultimodal } from "@/lib/db/ai-model-flags";
import {
getDefaultAiModel,
getEnabledAiModelsWithProvider,
getSystemSetting,
} from "@/lib/db/queries";
import {
normalizeProductPlanningAiModelConfig,
PRODUCT_PLANNING_AI_MODEL_CONFIG_KEY,
toProductPlanningOpenAIBaseUrl,
} from "./model-config";
type ProductPlanningModelResolution =
| {
model: LanguageModel;
source: "dedicated" | "default";
error?: undefined;
}
| {
model: null;
source: "dedicated" | "default";
error: string;
};
async function resolveDedicatedProductPlanningModel(): Promise<ProductPlanningModelResolution | null> {
const setting = await getSystemSetting({
key: PRODUCT_PLANNING_AI_MODEL_CONFIG_KEY,
}).catch(() => null);
const config = normalizeProductPlanningAiModelConfig(setting?.value);
if (!config.enabled) {
return null;
}
if (!config.apiKey.trim()) {
return {
model: null,
source: "dedicated",
error: "商品策划专用 AI 模型已启用,但未配置 API Key,请前往系统设置填写",
};
}
const provider = createOpenAI({
apiKey: config.apiKey.trim(),
baseURL: toProductPlanningOpenAIBaseUrl(config.baseUrl),
});
return {
model: provider.chat(config.modelId.trim()),
source: "dedicated",
};
}
async function resolveDefaultTextModel(): Promise<ProductPlanningModelResolution> {
const [defaultModel, enabledModels] = await Promise.all([
getDefaultAiModel().catch(() => undefined),
getEnabledAiModelsWithProvider(false),
]);
const candidates = enabledModels.filter(({ model, provider }) => {
const isVideoModel = isVideoModelDescriptor({
providerType: provider.type,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
});
return !isVideoModel && !model.supportsImageGen;
});
const selected =
(defaultModel ? candidates.find(({ model }) => model.id === defaultModel.id) : undefined) ||
candidates[0];
if (!selected) {
return {
model: null,
source: "default",
error: "当前站点没有可用的文本分析模型",
};
}
const model = await getLanguageModelById(selected.model.id);
if (!model) {
return {
model: null,
source: "default",
error: "当前站点没有可用的文本分析模型",
};
}
return {
model,
source: "default",
};
}
async function resolveDefaultVisionModel(): Promise<ProductPlanningModelResolution> {
const [defaultModel, enabledModels] = await Promise.all([
getDefaultAiModel().catch(() => undefined),
getEnabledAiModelsWithProvider(false),
]);
const candidates = enabledModels.filter(({ model, provider }) => {
const isVideoModel = isVideoModelDescriptor({
providerType: provider.type,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
});
const isImageModel = isImageGenerationModelDescriptor({
providerType: provider.type,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
});
return (
!isVideoModel &&
!isImageModel &&
(Boolean(model.supportsVision) ||
isAiModelMultimodal({
capabilities: model.capabilities,
defaultParams: model.defaultParams,
})) &&
!model.supportsImageGen
);
});
const selected =
(defaultModel ? candidates.find(({ model }) => model.id === defaultModel.id) : undefined) ||
candidates[0];
if (!selected) {
return {
model: null,
source: "default",
error: "当前站点没有可用的图片分析模型",
};
}
const model = await getLanguageModelById(selected.model.id);
if (!model) {
return {
model: null,
source: "default",
error: "当前站点没有可用的图片分析模型",
};
}
return {
model,
source: "default",
};
}
export async function resolveProductPlanningTextModel() {
const dedicated = await resolveDedicatedProductPlanningModel();
if (dedicated) {
return dedicated;
}
return resolveDefaultTextModel();
}
export async function resolveProductPlanningVisionModel() {
const dedicated = await resolveDedicatedProductPlanningModel();
if (dedicated) {
return dedicated;
}
return resolveDefaultVisionModel();
}