import type { AIProvider, AIProviderConfig, AIHealthResult, GenerateTextRequest, GenerateTextResponse, } from "../types"; import { isModelAvailable, resolveModelName } from "../model-resolve"; import { redactError, redactUrl } from "../types"; export function createOllamaProvider(): AIProvider { return { type: "ollama", async healthCheck(config: AIProviderConfig): Promise { const start = Date.now(); try { const res = await fetch(`${config.baseUrl}/api/tags`, { signal: AbortSignal.timeout(5000), }); const latencyMs = Date.now() - start; if (!res.ok) { return { status: "offline", provider: "ollama", model: config.model, latencyMs, lastSuccessAt: null, lastFailureAt: new Date().toISOString(), lastError: redactError(`HTTP ${res.status}`), baseUrlSafe: redactUrl(config.baseUrl), contextLength: null, memoryUsageMb: null, modelsAvailable: [], }; } const data = await res.json(); const models = (data.models ?? []).map( (m: { name: string }) => m.name ); const modelReady = isModelAvailable(config.model, models); const resolvedModel = resolveModelName(config.model, models); return { status: models.length === 0 ? "degraded" : modelReady || resolvedModel ? "online" : "degraded", provider: "ollama", model: resolvedModel ?? config.model, latencyMs, lastSuccessAt: new Date().toISOString(), lastFailureAt: modelReady ? null : new Date().toISOString(), lastError: modelReady ? null : models.length === 0 ? "No models installed in Ollama" : `Model "${config.model}" not found. Available: ${models.slice(0, 5).join(", ")}`, baseUrlSafe: redactUrl(config.baseUrl), contextLength: null, memoryUsageMb: null, modelsAvailable: models, }; } catch (e) { return { status: "offline", provider: "ollama", model: config.model, latencyMs: Date.now() - start, lastSuccessAt: null, lastFailureAt: new Date().toISOString(), lastError: redactError(e instanceof Error ? e.message : "Connection failed"), baseUrlSafe: redactUrl(config.baseUrl), contextLength: null, memoryUsageMb: null, modelsAvailable: [], }; } }, async listModels(config: AIProviderConfig) { const res = await fetch(`${config.baseUrl}/api/tags`, { signal: AbortSignal.timeout(5000), }); if (!res.ok) return []; const data = await res.json(); return (data.models ?? []).map((m: { name: string }) => m.name); }, async generateText( config: AIProviderConfig, req: GenerateTextRequest ): Promise { const start = Date.now(); const requested = req.model || config.model; let model = requested; const tagsRes = await fetch(`${config.baseUrl}/api/tags`, { signal: AbortSignal.timeout(5000), }); if (tagsRes.ok) { const tagsData = await tagsRes.json(); const models = (tagsData.models ?? []).map( (m: { name: string }) => m.name as string ); const resolved = resolveModelName(requested, models); if (resolved) { model = resolved; } else if (models.length === 0) { throw new Error(`Ollama error: no models installed`); } if (model !== requested) { console.error( `[adventureos-ai] ollama: resolved model "${requested}" -> "${model}"` ); } } const res = await fetch(`${config.baseUrl}/api/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model, prompt: req.prompt, system: req.system, stream: false, format: req.format === "json" ? "json" : undefined, options: { temperature: req.temperature ?? config.temperature, num_predict: req.maxTokens ?? config.maxTokens, }, }), signal: AbortSignal.timeout(req.timeoutMs ?? config.timeoutMs), }); if (!res.ok) { const body = await res.text().catch(() => ""); throw new Error( `Ollama error: HTTP ${res.status}${body ? ` — ${body.slice(0, 200)}` : ""}` ); } const data = await res.json(); return { text: data.response ?? "", model, latencyMs: Date.now() - start, }; }, }; }