Files
token/src/model.ts
T

50 lines
2.2 KiB
TypeScript

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<typeof toolCall>;
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<Message> {
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 };
}