You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
124 lines
3.9 KiB
124 lines
3.9 KiB
import { requireUser } from "#server/utils/context";
|
|
import { getProviderById, getModelById } from "#server/service/llm";
|
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
import { type LanguageModel, streamText, stepCountIs, convertToModelMessages } from 'ai';
|
|
import { getEnabledToolsForLlm } from "#server/service/agent-tool";
|
|
import log4js from "logger";
|
|
|
|
const logger = log4js.getLogger("APP");
|
|
|
|
function resolveModel(
|
|
provider: {
|
|
name: string;
|
|
apiKey: string | null;
|
|
baseUrl: string | null;
|
|
parseMode: string;
|
|
},
|
|
modelId: string,
|
|
): LanguageModel {
|
|
const baseUrl = provider.baseUrl?.replace(/\/+$/, "") || undefined;
|
|
|
|
if (provider.parseMode === "anthropic") {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Anthropic 解析模式暂不支持流式对话,请使用 OpenAI 兼容模式",
|
|
});
|
|
}
|
|
|
|
const openaiCompatible = createOpenAICompatible({
|
|
name: provider.name,
|
|
apiKey: provider.apiKey || undefined,
|
|
baseURL: baseUrl || "https://api.openai.com/v1",
|
|
});
|
|
|
|
return openaiCompatible(modelId) as LanguageModel;
|
|
}
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event);
|
|
if (!user) {
|
|
throw createError({ statusCode: 401, statusMessage: "未登录" });
|
|
}
|
|
|
|
const body = await readBody(event);
|
|
const { modelId: llmModelId, messages, enableThinking, enableTools } = body as {
|
|
modelId: number;
|
|
messages: any[];
|
|
enableThinking?: boolean;
|
|
enableTools?: boolean;
|
|
};
|
|
|
|
if (!llmModelId || !messages || !Array.isArray(messages) || messages.length === 0) {
|
|
throw createError({ statusCode: 400, statusMessage: "参数无效" });
|
|
}
|
|
|
|
const model = await getModelById(llmModelId, user.id);
|
|
if (!model) {
|
|
throw createError({ statusCode: 404, statusMessage: "模型不存在" });
|
|
}
|
|
|
|
const provider = await getProviderById(model.providerId, user.id);
|
|
if (!provider) {
|
|
throw createError({ statusCode: 404, statusMessage: "供应商不存在" });
|
|
}
|
|
|
|
if (provider.status !== "active") {
|
|
throw createError({ statusCode: 400, statusMessage: "供应商已禁用" });
|
|
}
|
|
|
|
if (!provider.apiKey) {
|
|
throw createError({ statusCode: 400, statusMessage: "供应商未配置 API Key" });
|
|
}
|
|
|
|
logger.info(
|
|
"[%s] [LLM-CHAT] userId=%d modelId=%d provider=%s parseMode=%s messages=%d thinking=%s tools=%s",
|
|
event.context.requestId ?? "-",
|
|
user.id,
|
|
llmModelId,
|
|
provider.name,
|
|
provider.parseMode,
|
|
messages.length,
|
|
enableThinking ? "on" : "off",
|
|
enableTools ? "on" : "off",
|
|
);
|
|
|
|
const languageModel = resolveModel(provider, model.modelId);
|
|
|
|
const { tools, approvalConfig } = enableTools ? await getEnabledToolsForLlm(user.id) : { tools: undefined, approvalConfig: {} };
|
|
|
|
const modelMessages = await convertToModelMessages(messages);
|
|
|
|
const result = streamText({
|
|
model: languageModel,
|
|
messages: modelMessages,
|
|
maxOutputTokens: model.maxTokens || undefined,
|
|
...(tools && Object.keys(tools).length > 0
|
|
? { tools, stopWhen: stepCountIs(8), toolApproval: approvalConfig }
|
|
: {}),
|
|
...(enableThinking
|
|
? {
|
|
providerOptions: {
|
|
openaiCompatible: { reasoningEffort: "high" },
|
|
},
|
|
}
|
|
: {}),
|
|
onError: (errorData) => {
|
|
const errMsg = errorData?.error instanceof Error
|
|
? errorData.error.message
|
|
: String(errorData?.error ?? "未知错误");
|
|
logger.error("[%s] [LLM-CHAT] streamText error: %s", event.context.requestId ?? "-", errMsg);
|
|
},
|
|
onFinish: ({ finishReason, usage, steps }) => {
|
|
logger.info(
|
|
"[%s] [LLM-CHAT] finished: reason=%s steps=%d inputTokens=%d outputTokens=%d",
|
|
event.context.requestId ?? "-",
|
|
finishReason,
|
|
steps.length,
|
|
usage?.inputTokens ?? 0,
|
|
usage?.outputTokens ?? 0,
|
|
);
|
|
},
|
|
});
|
|
|
|
return result.toUIMessageStreamResponse({ sendReasoning: true });
|
|
});
|
|
|