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, user.role) : { 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 }); });