providers.ts
1643 bytes
1import type { ChatMessage } from "./review";
2import type { ReviewerConfig } from "./types";
3
4type LlamaResponse = {
5 choices?: {
6 message?: {
7 content?: unknown;
8 };
9 }[];
10};
11
12function textValue(value: unknown): string | undefined {
13 return typeof value === "string" && value.trim() ? value : undefined;
14}
15
16function completionUrl(baseUrl: string): string {
17 const normalized = baseUrl.replace(/\/+$/, "");
18 const root = normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
19 return `${root}/chat/completions`;
20}
21
22async function responseError(response: Response): Promise<never> {
23 const detail = (await response.text()).trim().slice(0, 1000);
24 throw new Error(`llama-server API ${response.status}${detail ? `: ${detail}` : ""}`);
25}
26
27export async function callLlamaReviewer(
28 config: Extract<ReviewerConfig, { kind: "llama.cpp" }>,
29 messages: ChatMessage[],
30 maxTokens: number,
31): Promise<string> {
32 const response = await fetch(completionUrl(config.baseUrl), {
33 method: "POST",
34 headers: { "content-type": "application/json" },
35 body: JSON.stringify({
36 model: config.model,
37 max_tokens: maxTokens,
38 messages,
39 stream: false,
40 temperature: 0,
41 response_format: { type: "json_object" },
42 chat_template_kwargs: { enable_thinking: config.enableThinking },
43 reasoning_format: "deepseek",
44 }),
45 });
46 if (!response.ok) return responseError(response);
47 const data = (await response.json()) as LlamaResponse;
48 const text = textValue(data.choices?.[0]?.message?.content);
49 if (!text) throw new Error("llama-server response had no generated text");
50 return text;
51}