Files
GoClaw/server/ollama.ts
Manus 46e384c341 Checkpoint: Phase 8 Complete: Fix Orchestrator Chat
Исправлено:
- Chat.tsx: убрана хардкодированная модель "qwen2.5:7b" из мутации — теперь оркестратор использует модель из конфига БД (minimax-m2.7)
- Chat.tsx: добавлен Streamdown для markdown рендеринга ответов оркестратора
- Подтверждено: tool calling работает — команда "Покажи файлы проекта" вызывает file_list и возвращает структуру проекта
- Подтверждено: model в header показывает "minimax-m2.7" из БД
- TypeScript: 0 ошибок (pnpm tsc --noEmit)
- Тесты: 24/24 passed
2026-03-20 18:20:37 -04:00

170 lines
4.2 KiB
TypeScript

/**
* 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<string, unknown>;
}
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<string, string> {
const headers: Record<string, string> = {
"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<OllamaModelsResponse> {
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<ChatCompletionResponse> {
const body: Record<string, unknown> = {
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 };