import { z } from "zod"; import type { Config } from "./config.js"; export interface Message { role: "system" | "user" | "assistant" | "tool"; content: string | null | Array<{ type: "text"; text: string } | { type: "image_url"; image_url: { url: string } }>; tool_call_id?: string; tool_calls?: ToolCall[]; reasoning_content?: string; } const toolCall = z.object({ id: z.string().min(1), type: z.literal("function"), function: z.object({ name: z.string(), arguments: z.string() }), }); export type ToolCall = z.infer; const responseSchema = z.object({ choices: z.array(z.object({ finish_reason: z.string().nullable(), message: z.object({ role: z.literal("assistant"), content: z.string().nullable().optional(), reasoning_content: z.string().optional(), tool_calls: z.array(toolCall).optional(), }), })).min(1), }); export async function complete(config: Config, messages: Message[], tools: unknown[], signal?: AbortSignal): Promise { const response = await fetch(`${config.llamaCppOrigin}/v1/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: config.modelId, messages, tools, tool_choice: "auto", max_tokens: 2048, stream: false }), signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(600_000)]) : AbortSignal.timeout(600_000), }); if (!response.ok) { throw new Error(`llama.cpp HTTP ${response.status}: ${(await response.text()).slice(0, 2000)}`); } const result = responseSchema.parse(await response.json()); const choice = result.choices[0]; if (!choice) { throw new Error("Model returned no choice."); } if (choice.finish_reason !== "stop" && choice.finish_reason !== "tool_calls") { throw new Error(`Model response incomplete (${choice.finish_reason}); no tools executed.`); } const ids = choice.message.tool_calls?.map((call) => call.id) ?? []; if (new Set(ids).size !== ids.length) { throw new Error("Model returned duplicate tool-call IDs."); } return { ...choice.message, content: choice.message.content ?? null }; }