Skip to content

Commit 597211b

Browse files
committed
feat: implement Ollama provider for LLM abstraction
1 parent d73389a commit 597211b

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

packages/agent/src/core/llm/provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { AnthropicProvider } from './providers/anthropic.js';
6+
import { OllamaProvider } from './providers/ollama.js';
67
import { ProviderOptions, GenerateOptions, LLMResponse } from './types.js';
78

89
/**
@@ -39,6 +40,7 @@ const providerFactories: Record<
3940
(model: string, options: ProviderOptions) => LLMProvider
4041
> = {
4142
anthropic: (model, options) => new AnthropicProvider(model, options),
43+
ollama: (model, options) => new OllamaProvider(model, options),
4244
};
4345

4446
/**
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* Ollama provider implementation
3+
*/
4+
5+
import { TokenUsage } from '../../tokens.js';
6+
import { LLMProvider } from '../provider.js';
7+
import {
8+
GenerateOptions,
9+
LLMResponse,
10+
Message,
11+
ProviderOptions,
12+
} from '../types.js';
13+
14+
/**
15+
* Ollama-specific options
16+
*/
17+
export interface OllamaOptions extends ProviderOptions {
18+
baseUrl?: string;
19+
}
20+
21+
/**
22+
* Ollama provider implementation
23+
*/
24+
export class OllamaProvider implements LLMProvider {
25+
name: string = 'ollama';
26+
provider: string = 'ollama.chat';
27+
model: string;
28+
private baseUrl: string;
29+
30+
constructor(model: string, options: OllamaOptions = {}) {
31+
this.model = model;
32+
this.baseUrl = options.baseUrl || process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
33+
34+
// Ensure baseUrl doesn't end with a slash
35+
if (this.baseUrl.endsWith('/')) {
36+
this.baseUrl = this.baseUrl.slice(0, -1);
37+
}
38+
}
39+
40+
/**
41+
* Generate text using Ollama API
42+
*/
43+
async generateText(options: GenerateOptions): Promise<LLMResponse> {
44+
const { messages, functions, temperature = 0.7, maxTokens, topP, frequencyPenalty, presencePenalty } = options;
45+
46+
// Format messages for Ollama API
47+
const formattedMessages = this.formatMessages(messages);
48+
49+
try {
50+
// Prepare request options
51+
const requestOptions: any = {
52+
model: this.model,
53+
messages: formattedMessages,
54+
stream: false,
55+
options: {
56+
temperature: temperature,
57+
// Ollama uses top_k instead of top_p, but we'll include top_p if provided
58+
...(topP !== undefined && { top_p: topP }),
59+
...(frequencyPenalty !== undefined && { frequency_penalty: frequencyPenalty }),
60+
...(presencePenalty !== undefined && { presence_penalty: presencePenalty }),
61+
},
62+
};
63+
64+
// Add max_tokens if provided
65+
if (maxTokens !== undefined) {
66+
requestOptions.options.num_predict = maxTokens;
67+
}
68+
69+
// Add functions/tools if provided
70+
if (functions && functions.length > 0) {
71+
requestOptions.tools = functions.map((fn) => ({
72+
name: fn.name,
73+
description: fn.description,
74+
parameters: fn.parameters,
75+
}));
76+
}
77+
78+
// Make the API request
79+
const response = await fetch(`${this.baseUrl}/api/chat`, {
80+
method: 'POST',
81+
headers: {
82+
'Content-Type': 'application/json',
83+
},
84+
body: JSON.stringify(requestOptions),
85+
});
86+
87+
if (!response.ok) {
88+
const errorText = await response.text();
89+
throw new Error(`Ollama API error: ${response.status} ${errorText}`);
90+
}
91+
92+
const data = await response.json();
93+
94+
// Extract content and tool calls
95+
const content = data.message?.content || '';
96+
const toolCalls = data.message?.tool_calls?.map((toolCall: any) => ({
97+
id: toolCall.id || `tool-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
98+
name: toolCall.name,
99+
content: JSON.stringify(toolCall.args || toolCall.arguments || {}),
100+
})) || [];
101+
102+
// Create token usage from response data
103+
const tokenUsage = new TokenUsage();
104+
tokenUsage.input = data.prompt_eval_count || 0;
105+
tokenUsage.output = data.eval_count || 0;
106+
107+
return {
108+
text: content,
109+
toolCalls: toolCalls,
110+
tokenUsage: tokenUsage,
111+
};
112+
} catch (error) {
113+
throw new Error(
114+
`Error calling Ollama API: ${(error as Error).message}`,
115+
);
116+
}
117+
}
118+
119+
/**
120+
* Format messages for Ollama API
121+
*/
122+
private formatMessages(messages: Message[]): any[] {
123+
return messages.map((msg) => {
124+
if (msg.role === 'user' || msg.role === 'assistant' || msg.role === 'system') {
125+
return {
126+
role: msg.role,
127+
content: msg.content,
128+
};
129+
} else if (msg.role === 'tool_result') {
130+
// Ollama expects tool results as a 'tool' role
131+
return {
132+
role: 'tool',
133+
content: msg.content,
134+
tool_call_id: msg.tool_use_id,
135+
};
136+
} else if (msg.role === 'tool_use') {
137+
// We'll convert tool_use to assistant messages with tool_calls
138+
return {
139+
role: 'assistant',
140+
content: '',
141+
tool_calls: [
142+
{
143+
id: msg.id,
144+
name: msg.name,
145+
arguments: msg.content,
146+
},
147+
],
148+
};
149+
}
150+
// Default fallback
151+
return {
152+
role: 'user',
153+
content: msg.content,
154+
};
155+
});
156+
}
157+
}

0 commit comments

Comments
 (0)