feat(agent): add llm assistant chat with api tools (task 88)
OpenAI-compatible client with mock fallback, tool-calling loop, and /assistant UI. Tools map to /api/v1/ endpoints per ADR 0006 direct-tools decision.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import type { ChatCompletionRequest, ChatCompletionResult, LlmClient } from "./types";
|
||||
|
||||
type OpenAiMessage = {
|
||||
role: string;
|
||||
content: string | null;
|
||||
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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user