Files
famapp/src/lib/llm/openai-compatible.ts
T
ginnoir c8db5475d3 feat(agent): add voice input and photo attachments to assistant
Wire mic through Whisper-compatible transcriptions on LLM_BASE_URL.

Photos upload to MinIO and reach the vision model as base64 image_url parts.
2026-07-05 02:01:48 -05:00

75 lines
2.0 KiB
TypeScript

import type { ChatCompletionRequest, ChatCompletionResult, ChatMessage, LlmClient } from "./types";
type OpenAiMessage = {
role: string;
content: ChatMessage["content"];
tool_calls?: Array<{
id: string;
type: "function";
function: { name: string; arguments: string };
}>;
tool_call_id?: string;
name?: string;
};
export function createOpenAiCompatibleClient(options: {
baseUrl: string;
apiKey: string | null;
model: string;
}): LlmClient {
const completionsUrl = `${options.baseUrl.replace(/\/$/, "")}/chat/completions`;
return {
async chatCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResult> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (options.apiKey) {
headers.Authorization = `Bearer ${options.apiKey}`;
}
const body = {
model: options.model,
messages: request.messages as OpenAiMessage[],
tools: request.tools,
tool_choice: request.tools?.length ? "auto" : undefined,
};
const response = await fetch(completionsUrl, {
method: "POST",
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`LLM request failed (${response.status}): ${detail.slice(0, 400)}`);
}
const payload = (await response.json()) as {
choices?: Array<{
finish_reason?: string | null;
message?: OpenAiMessage;
}>;
};
const choice = payload.choices?.[0];
const message = choice?.message;
if (!message) {
throw new Error("LLM response missing message");
}
return {
message: {
role: message.role as ChatCompletionResult["message"]["role"],
content: message.content,
tool_calls: message.tool_calls,
tool_call_id: message.tool_call_id,
name: message.name,
},
finishReason: choice.finish_reason ?? null,
};
},
};
}