'use strict'; const axios = require('axios'); axios.defaults.timeout = 0; class OllamaProvider { constructor(config) { this.config = config; this.baseUrl = config.baseUrl || 'http://localhost:11434'; this.model = config.model || 'llama3.2'; this.messages = []; this.tools = []; } supportsTools() { return true; } setTools(tools) { this.tools = tools; } async start(history) { this.messages = history || []; if (this.config.prompt) { console.log('Ollama provider initialized with model:', this.model); } } __settings() { return { temperature: this.config.temperature || 1, top_p: this.config.topP || 0.95, top_k: this.config.topK || 64, num_predict: this.config.maxOutputTokens || 2048, num_ctx: this.config.num_ctx, }; } __jsonFormat() { return { type: 'array', items: { type: 'object', properties: { text: { type: 'string' }, delay: { type: 'number' } }, required: ['text', 'delay'] } }; } /** * Strip markdown code fences (```json ... ```) from a response string. * Ollama models (especially smaller ones) sometimes wrap their output in fences * even when the system prompt says not to. */ static stripMarkdownFences(text) { if (!text || typeof text !== 'string') return text; let cleaned = text.trim(); // Remove leading ```json or ``` fences cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, ''); // Remove trailing ``` fences cleaned = cleaned.replace(/\n?```\s*$/, ''); return cleaned.trim(); } async chat(message, retryCount = 0) { try { const messages = [ { role: 'system', content: this.config.prompt || 'You are a helpful assistant.' }, ...this.messages.map(msg => ({ role: msg.role === 'model' ? 'assistant' : 'user', content: msg.parts ? msg.parts.map(p => p.text).join('') : (msg.content || '') })), { role: 'user', content: message } ]; const requestBody = { model: this.model, messages: messages, stream: false, options: this.__settings() }; // Only set format when NO tools are configured. // format + tools together confuses smaller models — they try to // satisfy both constraints and produce garbage (_/empty responses). const hasTools = this.tools && this.tools.length > 0; if (!hasTools) { requestBody.format = this.__jsonFormat(); } if (hasTools) { requestBody.tools = this.tools.map(tool => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters } })); } const response = await axios.post( `${this.baseUrl}/api/chat`, requestBody, { headers: { 'Content-Type': 'application/json' } } ); const rawContent = response.data.message.content; const messageData = response.data.message; console.log('Ollama response', rawContent) // Update history this.messages.push({ role: 'user', parts: [{ text: message }], content: message }); this.messages.push({ role: 'model', parts: [{ text: rawContent }], content: rawContent }); // The text() closure strips markdown code fences so consumers // (processResponse, getToolCalls) get clean content. const result = { response: { text: () => { let content = messageData.content || rawContent; content = OllamaProvider.stripMarkdownFences(content); return content; } } }; // Ollama may return tool calls in messageData.tool_calls if (messageData.tool_calls) { result.tool_calls = messageData.tool_calls; } return result; } catch (error) { const errorDetails = { message: error.message, status: error.response?.status, data: error.response?.data, url: error.config?.url, retryCount: retryCount }; console.log('Ollama API error details:', errorDetails); if (retryCount > 3) { throw new Error(`Ollama API error after ${retryCount} retries: ${error.message}`); } const baseDelay = Math.min(1000 * Math.pow(2, retryCount), 30000); const jitter = Math.random() * 1000; await new Promise(resolve => setTimeout(resolve, baseDelay + jitter)); return await this.chat(message, retryCount + 1); } } setPrompt(prompt) { this.config.prompt = prompt; } getResponse(result) { return result.response.text(); } async close() { this.messages = []; } } module.exports = OllamaProvider;