1
1
/**
2
- * Ollama provider implementation
2
+ * Ollama provider implementation using the official Ollama npm package
3
3
*/
4
4
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
+
5
14
import { TokenUsage } from '../../tokens.js' ;
15
+ import { ToolCall } from '../../types.js' ;
6
16
import { LLMProvider } from '../provider.js' ;
7
17
import {
8
18
GenerateOptions ,
9
19
LLMResponse ,
10
20
Message ,
11
21
ProviderOptions ,
22
+ FunctionDefinition ,
12
23
} from '../types.js' ;
13
24
14
25
/**
@@ -19,29 +30,26 @@ export interface OllamaOptions extends ProviderOptions {
19
30
}
20
31
21
32
/**
22
- * Ollama provider implementation
33
+ * Ollama provider implementation using the official Ollama npm package
23
34
*/
24
35
export class OllamaProvider implements LLMProvider {
25
36
name : string = 'ollama' ;
26
37
provider : string = 'ollama.chat' ;
27
38
model : string ;
28
- private baseUrl : string ;
39
+ private client : Ollama ;
29
40
30
41
constructor ( model : string , options : OllamaOptions = { } ) {
31
42
this . model = model ;
32
- this . baseUrl =
43
+ const baseUrl =
33
44
options . baseUrl ||
34
45
process . env . OLLAMA_BASE_URL ||
35
46
'http://localhost:11434' ;
36
47
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 } ) ;
41
49
}
42
50
43
51
/**
44
- * Generate text using Ollama API
52
+ * Generate text using Ollama API via the official npm package
45
53
*/
46
54
async generateText ( options : GenerateOptions ) : Promise < LLMResponse > {
47
55
const {
@@ -52,126 +60,171 @@ export class OllamaProvider implements LLMProvider {
52
60
topP,
53
61
frequencyPenalty,
54
62
presencePenalty,
63
+ stopSequences,
55
64
} = options ;
56
65
57
66
// Format messages for Ollama API
58
67
const formattedMessages = this . formatMessages ( messages ) ;
59
68
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 ,
77
93
} ;
94
+ }
78
95
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
+ }
83
100
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
+ } ) ;
92
106
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 ;
106
117
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
+ }*/
108
144
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
+ }
119
163
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
+ }
124
171
172
+ return response . message . tool_calls . map ( ( toolCall : OllamaTooCall ) => {
173
+ //console.log('ollama tool call', toolCall);
125
174
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 || { } ) ,
129
181
} ;
130
- } catch ( error ) {
131
- throw new Error ( `Error calling Ollama API: ${ ( error as Error ) . message } ` ) ;
132
- }
182
+ } ) ;
133
183
}
134
184
135
185
/**
136
186
* Format messages for Ollama API
137
187
*/
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 = [
162
216
{
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
+ } ,
166
221
} ,
167
- ] ,
168
- } ;
222
+ ] ;
223
+ break ;
224
+ }
169
225
}
170
- // Default fallback for unknown message types
171
- return {
172
- role : 'user' ,
173
- content : ( msg as any ) . content || '' ,
174
- } ;
175
226
} ) ;
227
+
228
+ return output ;
176
229
}
177
230
}
0 commit comments