2
2
* Ollama provider implementation using the official Ollama npm package
3
3
*/
4
4
5
- import ollama , { Ollama , ChatResponse , Tool } from 'ollama' ;
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
+
6
14
import { TokenUsage } from '../../tokens.js' ;
15
+ import { ToolCall } from '../../types.js' ;
7
16
import { LLMProvider } from '../provider.js' ;
8
17
import {
9
- FunctionDefinition ,
10
18
GenerateOptions ,
11
19
LLMResponse ,
12
20
Message ,
13
21
ProviderOptions ,
22
+ FunctionDefinition ,
14
23
} from '../types.js' ;
15
24
16
25
/**
@@ -31,9 +40,9 @@ export class OllamaProvider implements LLMProvider {
31
40
32
41
constructor ( model : string , options : OllamaOptions = { } ) {
33
42
this . model = model ;
34
- const baseUrl =
35
- options . baseUrl ||
36
- process . env . OLLAMA_BASE_URL ||
43
+ const baseUrl =
44
+ options . baseUrl ||
45
+ process . env . OLLAMA_BASE_URL ||
37
46
'http://localhost:11434' ;
38
47
39
48
this . client = new Ollama ( { host : baseUrl } ) ;
@@ -57,133 +66,165 @@ export class OllamaProvider implements LLMProvider {
57
66
// Format messages for Ollama API
58
67
const formattedMessages = this . formatMessages ( messages ) ;
59
68
60
- try {
61
- // Prepare chat options
62
- const ollamaOptions : Record < string , any > = {
63
- temperature,
64
- } ;
65
-
66
- // Add optional parameters if provided
67
- if ( topP !== undefined ) ollamaOptions . top_p = topP ;
68
- if ( frequencyPenalty !== undefined ) ollamaOptions . frequency_penalty = frequencyPenalty ;
69
- if ( presencePenalty !== undefined ) ollamaOptions . presence_penalty = presencePenalty ;
70
- if ( maxTokens !== undefined ) ollamaOptions . num_predict = maxTokens ;
71
- if ( stopSequences && stopSequences . length > 0 ) ollamaOptions . stop = stopSequences ;
72
-
73
- // Prepare request parameters
74
- const requestParams : any = {
75
- model : this . model ,
76
- messages : formattedMessages ,
77
- stream : false ,
78
- options : ollamaOptions ,
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 ,
79
93
} ;
94
+ }
80
95
81
- // Add functions/tools if provided
82
- if ( functions && functions . length > 0 ) {
83
- requestParams . tools = this . convertFunctionsToTools ( functions ) ;
84
- }
96
+ // Add functions/tools if provided
97
+ if ( functions && functions . length > 0 ) {
98
+ requestOptions . tools = this . convertFunctionsToTools ( functions ) ;
99
+ }
85
100
86
- // Make the API request using the Ollama client
87
- const response = await this . client . chat ( requestParams ) ;
101
+ // Make the API request using the Ollama client
102
+ const response : OllamaChatResponse = await this . client . chat ( {
103
+ ...requestOptions ,
104
+ stream : false ,
105
+ } ) ;
88
106
89
- // Extract content from response
90
- const content = response . message ?. content || '' ;
91
-
92
- // Process tool calls if present
93
- const toolCalls = this . processToolCalls ( response ) ;
107
+ // Extract content and tool calls
108
+ const content = response . message ?. content || '' ;
94
109
95
- // Create token usage from response data
96
- const tokenUsage = new TokenUsage ( ) ;
97
- if ( response . prompt_eval_count ) {
98
- tokenUsage . input = response . prompt_eval_count ;
99
- }
100
- if ( response . eval_count ) {
101
- tokenUsage . output = response . eval_count ;
102
- }
110
+ // Handle tool calls if present
111
+ const toolCalls = this . extractToolCalls ( response ) ;
103
112
104
- return {
105
- text : content ,
106
- toolCalls : toolCalls ,
107
- tokenUsage : tokenUsage ,
108
- } ;
109
- } catch ( error ) {
110
- throw new Error ( `Error calling Ollama API: ${ ( error as Error ) . message } ` ) ;
111
- }
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 ;
117
+
118
+ return {
119
+ text : content ,
120
+ toolCalls : toolCalls ,
121
+ tokenUsage : tokenUsage ,
122
+ } ;
112
123
}
113
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
+ }*/
144
+
114
145
/**
115
- * Convert our FunctionDefinition format to Ollama's Tool format
146
+ * Convert our function definitions to Ollama tool format
116
147
*/
117
- private convertFunctionsToTools ( functions : FunctionDefinition [ ] ) : Tool [ ] {
118
- return functions . map ( ( fn ) => ( {
119
- type : 'function' ,
120
- function : {
121
- name : fn . name ,
122
- description : fn . description ,
123
- parameters : fn . parameters ,
124
- }
125
- } ) ) ;
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
+ ) ;
126
162
}
127
163
128
164
/**
129
- * Process tool calls from the Ollama response
165
+ * Extract tool calls from Ollama response
130
166
*/
131
- private processToolCalls ( response : ChatResponse ) : any [ ] {
132
- if ( ! response . message ?. tool_calls || response . message . tool_calls . length === 0 ) {
167
+ private extractToolCalls ( response : OllamaChatResponse ) : ToolCall [ ] {
168
+ if ( ! response . message ?. tool_calls ) {
133
169
return [ ] ;
134
170
}
135
171
136
- return response . message . tool_calls . map ( ( toolCall ) => ( {
137
- id : toolCall . function ?. name
138
- ? `tool-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 11 ) } `
139
- : toolCall . id ,
140
- name : toolCall . function ?. name ,
141
- content : JSON . stringify ( toolCall . function ?. arguments || { } ) ,
142
- } ) ) ;
172
+ return response . message . tool_calls . map ( ( toolCall : OllamaTooCall ) => {
173
+ //console.log('ollama tool call', toolCall);
174
+ return {
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 || { } ) ,
181
+ } ;
182
+ } ) ;
143
183
}
144
184
145
185
/**
146
186
* Format messages for Ollama API
147
187
*/
148
- private formatMessages ( messages : Message [ ] ) : any [ ] {
149
- return messages . map ( ( msg ) => {
150
- if (
151
- msg . role === 'user' ||
152
- msg . role === 'assistant' ||
153
- msg . role === 'system'
154
- ) {
155
- return {
156
- role : msg . role ,
157
- content : msg . content ,
158
- } ;
159
- } else if ( msg . role === 'tool_result' ) {
160
- // Ollama expects tool results as a 'tool' role
161
- return {
162
- role : 'tool' ,
163
- content : msg . content ,
164
- tool_call_id : msg . tool_use_id ,
165
- } ;
166
- } else if ( msg . role === 'tool_use' ) {
167
- // We'll convert tool_use to assistant messages with tool_calls
168
- return {
169
- role : 'assistant' ,
170
- content : '' ,
171
- 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 = [
172
216
{
173
- id : msg . id ,
174
217
function : {
175
218
name : msg . name ,
176
- arguments : msg . content ,
177
- }
219
+ arguments : JSON . parse ( msg . content ) ,
220
+ } ,
178
221
} ,
179
- ] ,
180
- } ;
222
+ ] ;
223
+ break ;
224
+ }
181
225
}
182
- // Default fallback for unknown message types
183
- return {
184
- role : 'user' ,
185
- content : ( msg as any ) . content || '' ,
186
- } ;
187
226
} ) ;
227
+
228
+ return output ;
188
229
}
189
- }
230
+ }
0 commit comments