11/**
2- * Ollama provider implementation
2+ * Ollama provider implementation using the official Ollama npm package
33 */
44
5+ import {
6+ ChatRequest as OllamaChatRequest,
7+ ChatResponse as OllamaChatResponse,
8+ Ollama,
9+ ToolCall as OllamaTooCall,
10+ Tool as OllamaTool,
11+ Message as OllamaMessage,
12+ } from 'ollama';
13+
514import { TokenUsage } from '../../tokens.js';
15+ import { ToolCall } from '../../types.js';
616import { LLMProvider } from '../provider.js';
717import {
818 GenerateOptions,
919 LLMResponse,
1020 Message,
1121 ProviderOptions,
22+ FunctionDefinition,
1223} from '../types.js';
1324
1425/**
@@ -19,29 +30,26 @@ export interface OllamaOptions extends ProviderOptions {
1930}
2031
2132/**
22- * Ollama provider implementation
33+ * Ollama provider implementation using the official Ollama npm package
2334 */
2435export class OllamaProvider implements LLMProvider {
2536 name: string = 'ollama';
2637 provider: string = 'ollama.chat';
2738 model: string;
28- private baseUrl: string ;
39+ private client: Ollama ;
2940
3041 constructor(model: string, options: OllamaOptions = {}) {
3142 this.model = model;
32- this. baseUrl =
43+ const baseUrl =
3344 options.baseUrl ||
3445 process.env.OLLAMA_BASE_URL ||
3546 'http://localhost:11434';
3647
37- // Ensure baseUrl doesn't end with a slash
38- if (this.baseUrl.endsWith('/')) {
39- this.baseUrl = this.baseUrl.slice(0, -1);
40- }
48+ this.client = new Ollama({ host: baseUrl });
4149 }
4250
4351 /**
44- * Generate text using Ollama API
52+ * Generate text using Ollama API via the official npm package
4553 */
4654 async generateText(options: GenerateOptions): Promise<LLMResponse> {
4755 const {
@@ -52,126 +60,171 @@ export class OllamaProvider implements LLMProvider {
5260 topP,
5361 frequencyPenalty,
5462 presencePenalty,
63+ stopSequences,
5564 } = options;
5665
5766 // Format messages for Ollama API
5867 const formattedMessages = this.formatMessages(messages);
5968
60- try {
61- // Prepare request options
62- const requestOptions: any = {
63- model: this.model,
64- messages: formattedMessages,
65- stream: false,
66- options: {
67- temperature: temperature,
68- // Ollama uses top_k instead of top_p, but we'll include top_p if provided
69- ...(topP !== undefined && { top_p: topP }),
70- ...(frequencyPenalty !== undefined && {
71- frequency_penalty: frequencyPenalty,
72- }),
73- ...(presencePenalty !== undefined && {
74- presence_penalty: presencePenalty,
75- }),
76- },
69+ // Prepare request options
70+ const requestOptions: OllamaChatRequest = {
71+ model: this.model,
72+ messages: formattedMessages,
73+ stream: false,
74+ options: {
75+ temperature: temperature,
76+ ...(topP !== undefined && { top_p: topP }),
77+ ...(frequencyPenalty !== undefined && {
78+ frequency_penalty: frequencyPenalty,
79+ }),
80+ ...(presencePenalty !== undefined && {
81+ presence_penalty: presencePenalty,
82+ }),
83+ ...(stopSequences &&
84+ stopSequences.length > 0 && { stop: stopSequences }),
85+ },
86+ };
87+
88+ // Add max_tokens if provided
89+ if (maxTokens !== undefined) {
90+ requestOptions.options = {
91+ ...requestOptions.options,
92+ num_predict: maxTokens,
7793 };
94+ }
7895
79- // Add max_tokens if provided
80- if (maxTokens !== undefined ) {
81- requestOptions.options.num_predict = maxTokens ;
82- }
96+ // Add functions/tools if provided
97+ if (functions && functions.length > 0 ) {
98+ requestOptions.tools = this.convertFunctionsToTools(functions) ;
99+ }
83100
84- // Add functions/tools if provided
85- if (functions && functions.length > 0) {
86- requestOptions.tools = functions.map((fn) => ({
87- name: fn.name,
88- description: fn.description,
89- parameters: fn.parameters,
90- }));
91- }
101+ // Make the API request using the Ollama client
102+ const response: OllamaChatResponse = await this.client.chat({
103+ ...requestOptions,
104+ stream: false,
105+ });
92106
93- // Make the API request
94- const response = await fetch(`${this.baseUrl}/api/chat`, {
95- method: 'POST',
96- headers: {
97- 'Content-Type': 'application/json',
98- },
99- body: JSON.stringify(requestOptions),
100- });
101-
102- if (!response.ok) {
103- const errorText = await response.text();
104- throw new Error(`Ollama API error: ${response.status} ${errorText}`);
105- }
107+ // Extract content and tool calls
108+ const content = response.message?.content || '';
109+
110+ // Handle tool calls if present
111+ const toolCalls = this.extractToolCalls(response);
112+
113+ // Create token usage from response data
114+ const tokenUsage = new TokenUsage();
115+ tokenUsage.output = response.eval_count || 0;
116+ tokenUsage.input = response.prompt_eval_count || 0;
106117
107- const data = await response.json();
118+ return {
119+ text: content,
120+ toolCalls: toolCalls,
121+ tokenUsage: tokenUsage,
122+ };
123+ }
124+
125+ /*
126+ interface Tool {
127+ type: string;
128+ function: {
129+ name: string;
130+ description: string;
131+ parameters: {
132+ type: string;
133+ required: string[];
134+ properties: {
135+ [key: string]: {
136+ type: string;
137+ description: string;
138+ enum?: string[];
139+ };
140+ };
141+ };
142+ };
143+ }*/
108144
109- // Extract content and tool calls
110- const content = data.message?.content || '';
111- const toolCalls =
112- data.message?.tool_calls?.map((toolCall: any) => ({
113- id:
114- toolCall.id ||
115- `tool-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
116- name: toolCall.name,
117- content: JSON.stringify(toolCall.args || toolCall.arguments || {}),
118- })) || [];
145+ /**
146+ * Convert our function definitions to Ollama tool format
147+ */
148+ private convertFunctionsToTools(
149+ functions: FunctionDefinition[],
150+ ): OllamaTool[] {
151+ return functions.map(
152+ (fn) =>
153+ ({
154+ type: 'function',
155+ function: {
156+ name: fn.name,
157+ description: fn.description,
158+ parameters: fn.parameters,
159+ },
160+ }) as OllamaTool,
161+ );
162+ }
119163
120- // Create token usage from response data
121- const tokenUsage = new TokenUsage();
122- tokenUsage.input = data.prompt_eval_count || 0;
123- tokenUsage.output = data.eval_count || 0;
164+ /**
165+ * Extract tool calls from Ollama response
166+ */
167+ private extractToolCalls(response: OllamaChatResponse): ToolCall[] {
168+ if (!response.message?.tool_calls) {
169+ return [];
170+ }
124171
172+ return response.message.tool_calls.map((toolCall: OllamaTooCall) => {
173+ //console.log('ollama tool call', toolCall);
125174 return {
126- text: content,
127- toolCalls: toolCalls,
128- tokenUsage: tokenUsage,
175+ id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
176+ name: toolCall.function?.name,
177+ content:
178+ typeof toolCall.function?.arguments === 'string'
179+ ? toolCall.function.arguments
180+ : JSON.stringify(toolCall.function?.arguments || {}),
129181 };
130- } catch (error) {
131- throw new Error(`Error calling Ollama API: ${(error as Error).message}`);
132- }
182+ });
133183 }
134184
135185 /**
136186 * Format messages for Ollama API
137187 */
138- private formatMessages(messages: Message[]): any[] {
139- return messages.map((msg) => {
140- if (
141- msg.role === 'user' ||
142- msg.role === 'assistant' ||
143- msg.role === 'system'
144- ) {
145- return {
146- role: msg.role,
147- content: msg.content,
148- };
149- } else if (msg.role === 'tool_result') {
150- // Ollama expects tool results as a 'tool' role
151- return {
152- role: 'tool',
153- content: msg.content,
154- tool_call_id: msg.tool_use_id,
155- };
156- } else if (msg.role === 'tool_use') {
157- // We'll convert tool_use to assistant messages with tool_calls
158- return {
159- role: 'assistant',
160- content: '',
161- tool_calls: [
188+ private formatMessages(messages: Message[]): OllamaMessage[] {
189+ const output: OllamaMessage[] = [];
190+
191+ messages.forEach((msg) => {
192+ switch (msg.role) {
193+ case 'user':
194+ case 'assistant':
195+ case 'system':
196+ output.push({
197+ role: msg.role,
198+ content: msg.content,
199+ } satisfies OllamaMessage);
200+ break;
201+ case 'tool_result':
202+ // Ollama expects tool results as a 'tool' role
203+ output.push({
204+ role: 'tool',
205+ content:
206+ typeof msg.content === 'string'
207+ ? msg.content
208+ : JSON.stringify(msg.content),
209+ } as OllamaMessage);
210+ break;
211+ case 'tool_use': {
212+ // So there is an issue here is that ollama expects tool calls to be part of the assistant message
213+ // get last message and add tool call to it
214+ const lastMessage: OllamaMessage = output[output.length - 1]!;
215+ lastMessage.tool_calls = [
162216 {
163- id: msg.id,
164- name: msg.name,
165- arguments: msg.content,
217+ function: {
218+ name: msg.name,
219+ arguments: JSON.parse(msg.content),
220+ },
166221 },
167- ],
168- };
222+ ];
223+ break;
224+ }
169225 }
170- // Default fallback for unknown message types
171- return {
172- role: 'user',
173- content: (msg as any).content || '',
174- };
175226 });
227+
228+ return output;
176229 }
177230}
0 commit comments