import { isAnthropicCompatibleBaseUrl, normalizeLlmConfig, type LlmConfig, } from "./llm-config"; import { compactTracePreview } from "./chat-trace"; export type LlmCallResult = { text: string; endpoint: string; requestPreview: string; responsePreview: string; }; function extractJsonText(value: unknown): string | undefined { if (typeof value === "string") return value; if (Array.isArray(value)) { const joined = value .map((item) => { if (typeof item === "string") return item; if ( item && typeof item === "object" && "type" in item && "text" in item && item.type === "text" && typeof item.text === "string" ) { return item.text; } return ""; }) .filter(Boolean) .join("\n"); return joined || undefined; } return undefined; } function extractOpenAiText(payload: unknown): string | undefined { const data = payload as | { choices?: Array<{ message?: { content?: unknown }; text?: unknown; }>; } | undefined; const choice = data?.choices?.[0]; return extractJsonText(choice?.message?.content) ?? extractJsonText(choice?.text); } function extractAnthropicText(payload: unknown): string | undefined { const data = payload as | { content?: Array<{ type?: string; text?: string; }>; } | undefined; return data?.content ?.filter((item) => item.type === "text" && typeof item.text === "string") .map((item) => item.text) .join("\n"); } export async function generateLlmText(args: { config: LlmConfig; systemPrompt: string; userPrompt: string; temperature?: number; maxTokens?: number; responseFormat?: "json_object"; }): Promise { const { systemPrompt, userPrompt } = args; const config = normalizeLlmConfig(args.config); const temperature = args.temperature ?? 0.2; const maxTokens = args.maxTokens ?? 2200; const anthropicStyle = isAnthropicCompatibleBaseUrl(config.baseUrl); const responseFormat = args.responseFormat; const endpoint = anthropicStyle ? `${config.baseUrl}/v1/messages` : `${config.baseUrl}/chat/completions`; const response = await fetch(endpoint, { method: "POST", headers: anthropicStyle ? { "content-type": "application/json", "x-api-key": config.apiKey, "anthropic-version": "2023-06-01", } : { "content-type": "application/json", authorization: `Bearer ${config.apiKey}`, }, body: JSON.stringify( anthropicStyle ? { model: config.model, max_tokens: maxTokens, temperature, system: systemPrompt, messages: [ { role: "user", content: [ { type: "text", text: userPrompt, }, ], }, ], } : { model: config.model, temperature, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt }, ], ...(responseFormat ? { response_format: { type: responseFormat } } : {}), }, ), }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error(`LLM request failed (${response.status}): ${text}`.slice(0, 1200)); } const rawText = await response.text(); let payload: unknown = null; if (rawText.trim()) { try { payload = JSON.parse(rawText); } catch { payload = null; } } const text = anthropicStyle ? extractAnthropicText(payload) : extractOpenAiText(payload) ?? rawText.trim(); if (!text) { throw new Error("LLM returned an empty response."); } return { text, endpoint, requestPreview: compactTracePreview( `SYSTEM:\n${systemPrompt}\n\nUSER:\n${userPrompt}`, ), responsePreview: compactTracePreview(text), }; }