import { Logger } from '@nestjs/common'; import type { ModelRegistry } from '@mariozechner/pi-coding-agent'; import type { CompletionEvent, CompletionParams, IProviderAdapter, ModelInfo, ProviderHealth, } from '@mosaic/types'; /** * Ollama provider adapter. * * Registers local Ollama models with the Pi ModelRegistry via the OpenAI-compatible * completions API. Configuration is driven by environment variables: * OLLAMA_BASE_URL or OLLAMA_HOST — base URL of the Ollama instance * OLLAMA_MODELS — comma-separated list of model IDs (default: llama3.2,codellama,mistral) */ export class OllamaAdapter implements IProviderAdapter { readonly name = 'ollama'; private readonly logger = new Logger(OllamaAdapter.name); private registeredModels: ModelInfo[] = []; constructor(private readonly registry: ModelRegistry) {} async register(): Promise { const ollamaUrl = process.env['OLLAMA_BASE_URL'] ?? process.env['OLLAMA_HOST']; if (!ollamaUrl) { this.logger.debug('Skipping Ollama provider registration: OLLAMA_BASE_URL not set'); return; } const modelsEnv = process.env['OLLAMA_MODELS'] ?? 'llama3.2,codellama,mistral'; const modelIds = modelsEnv .split(',') .map((id: string) => id.trim()) .filter(Boolean); this.registry.registerProvider('ollama', { baseUrl: `${ollamaUrl}/v1`, apiKey: 'ollama', api: 'openai-completions' as never, models: modelIds.map((id) => ({ id, name: id, reasoning: false, input: ['text'] as ('text' | 'image')[], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 8192, maxTokens: 4096, })), }); this.registeredModels = modelIds.map((id) => ({ id, provider: 'ollama', name: id, reasoning: false, contextWindow: 8192, maxTokens: 4096, inputTypes: ['text'] as ('text' | 'image')[], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, })); this.logger.log( `Ollama provider registered at ${ollamaUrl} with models: ${modelIds.join(', ')}`, ); } listModels(): ModelInfo[] { return this.registeredModels; } async healthCheck(): Promise { const ollamaUrl = process.env['OLLAMA_BASE_URL'] ?? process.env['OLLAMA_HOST']; if (!ollamaUrl) { return { status: 'down', lastChecked: new Date().toISOString(), error: 'OLLAMA_BASE_URL not configured', }; } const checkUrl = `${ollamaUrl}/v1/models`; const start = Date.now(); try { const res = await fetch(checkUrl, { method: 'GET', headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(5000), }); const latencyMs = Date.now() - start; if (!res.ok) { return { status: 'degraded', latencyMs, lastChecked: new Date().toISOString(), error: `HTTP ${res.status}`, }; } return { status: 'healthy', latencyMs, lastChecked: new Date().toISOString() }; } catch (err) { const latencyMs = Date.now() - start; const error = err instanceof Error ? err.message : String(err); return { status: 'down', latencyMs, lastChecked: new Date().toISOString(), error }; } } /** * createCompletion is reserved for future direct-completion use. * The current integration routes completions through Pi SDK's ModelRegistry/AgentSession. */ async *createCompletion(_params: CompletionParams): AsyncIterable { throw new Error( 'OllamaAdapter.createCompletion is not yet implemented. ' + 'Use Pi SDK AgentSession for completions.', ); // Satisfy the AsyncGenerator return type — unreachable but required for TypeScript. yield undefined as never; } }