/** * Ollama API Client — серверный прокси для безопасного доступа к Ollama Cloud API. * Хранит API-ключ на сервере, обходит CORS, предоставляет типизированные функции. */ import { ENV } from "./_core/env"; const TIMEOUT_MS = 30_000; interface OllamaModel { id: string; object: string; created: number; owned_by: string; } interface OllamaModelsResponse { object: string; data: OllamaModel[]; } interface ChatMessage { role: "system" | "user" | "assistant" | "tool" | "function"; content: string | null; tool_call_id?: string; name?: string; tool_calls?: ToolCall[]; } interface ToolFunction { name: string; description?: string; parameters?: Record; } interface Tool { type: "function"; function: ToolFunction; } interface ToolCallFunction { name: string; arguments: string; } interface ToolCall { id: string; type: "function"; function: ToolCallFunction; } interface ChatChoice { index: number; message: { role: string; content: string | null; tool_calls?: ToolCall[]; }; finish_reason: string; } interface ChatCompletionResponse { id: string; object: string; created: number; model: string; choices: ChatChoice[]; usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; } function getHeaders(): Record { const headers: Record = { "Content-Type": "application/json", }; if (ENV.ollamaApiKey) { headers["Authorization"] = `Bearer ${ENV.ollamaApiKey}`; } return headers; } function getBaseUrl(): string { return ENV.ollamaBaseUrl.replace(/\/$/, ""); } /** * Проверка доступности Ollama API */ export async function checkOllamaHealth(): Promise<{ connected: boolean; latencyMs: number; error?: string; }> { const start = Date.now(); try { const res = await fetch(`${getBaseUrl()}/models`, { method: "GET", headers: getHeaders(), signal: AbortSignal.timeout(10_000), }); const latencyMs = Date.now() - start; if (res.ok) { return { connected: true, latencyMs }; } const text = await res.text(); return { connected: false, latencyMs, error: `HTTP ${res.status}: ${text}` }; } catch (err: any) { return { connected: false, latencyMs: Date.now() - start, error: err.message }; } } /** * Получение списка доступных моделей (OpenAI-совместимый формат) */ export async function listModels(): Promise { const res = await fetch(`${getBaseUrl()}/models`, { method: "GET", headers: getHeaders(), signal: AbortSignal.timeout(TIMEOUT_MS), }); if (!res.ok) { const text = await res.text(); throw new Error(`Ollama API error (${res.status}): ${text}`); } return res.json(); } /** * Отправка сообщения в чат (OpenAI-совместимый формат, без стриминга) * Поддерживает tool calling для оркестратора. */ export async function chatCompletion( model: string, messages: ChatMessage[], options?: { temperature?: number; max_tokens?: number; tools?: Tool[]; tool_choice?: "auto" | "none" | "required"; } ): Promise { const body: Record = { model, messages, stream: false, }; if (options?.temperature !== undefined) body.temperature = options.temperature; if (options?.max_tokens !== undefined) body.max_tokens = options.max_tokens; if (options?.tools?.length) { body.tools = options.tools; body.tool_choice = options.tool_choice ?? "auto"; } const res = await fetch(`${getBaseUrl()}/chat/completions`, { method: "POST", headers: getHeaders(), body: JSON.stringify(body), signal: AbortSignal.timeout(120_000), // LLM может думать долго }); if (!res.ok) { const text = await res.text(); throw new Error(`Ollama Chat API error (${res.status}): ${text}`); } return res.json(); } export type { OllamaModel, OllamaModelsResponse, ChatMessage, ChatCompletionResponse };