@auxiora/providers 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +191 -0
  2. package/dist/anthropic.d.ts +82 -0
  3. package/dist/anthropic.d.ts.map +1 -0
  4. package/dist/anthropic.js +618 -0
  5. package/dist/anthropic.js.map +1 -0
  6. package/dist/claude-code-tools.d.ts +29 -0
  7. package/dist/claude-code-tools.d.ts.map +1 -0
  8. package/dist/claude-code-tools.js +221 -0
  9. package/dist/claude-code-tools.js.map +1 -0
  10. package/dist/claude-oauth.d.ts +86 -0
  11. package/dist/claude-oauth.d.ts.map +1 -0
  12. package/dist/claude-oauth.js +318 -0
  13. package/dist/claude-oauth.js.map +1 -0
  14. package/dist/cohere.d.ts +18 -0
  15. package/dist/cohere.d.ts.map +1 -0
  16. package/dist/cohere.js +163 -0
  17. package/dist/cohere.js.map +1 -0
  18. package/dist/deepseek.d.ts +18 -0
  19. package/dist/deepseek.d.ts.map +1 -0
  20. package/dist/deepseek.js +164 -0
  21. package/dist/deepseek.js.map +1 -0
  22. package/dist/factory.d.ts +19 -0
  23. package/dist/factory.d.ts.map +1 -0
  24. package/dist/factory.js +108 -0
  25. package/dist/factory.js.map +1 -0
  26. package/dist/google.d.ts +18 -0
  27. package/dist/google.d.ts.map +1 -0
  28. package/dist/google.js +141 -0
  29. package/dist/google.js.map +1 -0
  30. package/dist/groq.d.ts +18 -0
  31. package/dist/groq.d.ts.map +1 -0
  32. package/dist/groq.js +186 -0
  33. package/dist/groq.js.map +1 -0
  34. package/dist/index.d.ts +15 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +14 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/ollama.d.ts +18 -0
  39. package/dist/ollama.d.ts.map +1 -0
  40. package/dist/ollama.js +141 -0
  41. package/dist/ollama.js.map +1 -0
  42. package/dist/openai-compatible.d.ts +20 -0
  43. package/dist/openai-compatible.d.ts.map +1 -0
  44. package/dist/openai-compatible.js +112 -0
  45. package/dist/openai-compatible.js.map +1 -0
  46. package/dist/openai.d.ts +20 -0
  47. package/dist/openai.d.ts.map +1 -0
  48. package/dist/openai.js +259 -0
  49. package/dist/openai.js.map +1 -0
  50. package/dist/replicate.d.ts +20 -0
  51. package/dist/replicate.d.ts.map +1 -0
  52. package/dist/replicate.js +186 -0
  53. package/dist/replicate.js.map +1 -0
  54. package/dist/thinking-levels.d.ts +16 -0
  55. package/dist/thinking-levels.d.ts.map +1 -0
  56. package/dist/thinking-levels.js +34 -0
  57. package/dist/thinking-levels.js.map +1 -0
  58. package/dist/types.d.ts +157 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/xai.d.ts +18 -0
  63. package/dist/xai.d.ts.map +1 -0
  64. package/dist/xai.js +164 -0
  65. package/dist/xai.js.map +1 -0
  66. package/package.json +30 -0
  67. package/src/anthropic.ts +691 -0
  68. package/src/claude-code-tools.ts +233 -0
  69. package/src/claude-oauth.ts +410 -0
  70. package/src/cohere.ts +242 -0
  71. package/src/deepseek.ts +241 -0
  72. package/src/factory.ts +142 -0
  73. package/src/google.ts +176 -0
  74. package/src/groq.ts +263 -0
  75. package/src/index.ts +44 -0
  76. package/src/ollama.ts +194 -0
  77. package/src/openai-compatible.ts +154 -0
  78. package/src/openai.ts +307 -0
  79. package/src/replicate.ts +247 -0
  80. package/src/thinking-levels.ts +37 -0
  81. package/src/types.ts +171 -0
  82. package/src/xai.ts +241 -0
  83. package/tests/adapters.test.ts +185 -0
  84. package/tests/claude-oauth.test.ts +45 -0
  85. package/tests/new-providers.test.ts +732 -0
  86. package/tests/thinking-levels.test.ts +82 -0
  87. package/tsconfig.json +8 -0
  88. package/tsconfig.tsbuildinfo +1 -0
package/src/google.ts ADDED
@@ -0,0 +1,176 @@
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
+ import type {
3
+ Provider,
4
+ ProviderMetadata,
5
+ ChatMessage,
6
+ CompletionOptions,
7
+ CompletionResult,
8
+ StreamChunk,
9
+ } from './types.js';
10
+
11
+ const DEFAULT_MODEL = 'gemini-2.5-flash';
12
+ const DEFAULT_MAX_TOKENS = 4096;
13
+
14
+ export interface GoogleProviderOptions {
15
+ apiKey: string;
16
+ model?: string;
17
+ maxTokens?: number;
18
+ }
19
+
20
+ export class GoogleProvider implements Provider {
21
+ name = 'google';
22
+ metadata: ProviderMetadata = {
23
+ name: 'google',
24
+ displayName: 'Google Gemini',
25
+ models: {
26
+ 'gemini-2.5-flash': {
27
+ maxContextTokens: 1048576,
28
+ supportsVision: true,
29
+ supportsTools: true,
30
+ supportsStreaming: true,
31
+ supportsImageGen: false,
32
+ costPer1kInput: 0.00015,
33
+ costPer1kOutput: 0.0006,
34
+ strengths: ['fast', 'code', 'reasoning'],
35
+ isLocal: false,
36
+ },
37
+ 'gemini-2.5-pro': {
38
+ maxContextTokens: 1048576,
39
+ supportsVision: true,
40
+ supportsTools: true,
41
+ supportsStreaming: true,
42
+ supportsImageGen: false,
43
+ costPer1kInput: 0.00125,
44
+ costPer1kOutput: 0.01,
45
+ strengths: ['reasoning', 'code', 'long-context', 'creative'],
46
+ isLocal: false,
47
+ },
48
+ },
49
+ isAvailable: async () => {
50
+ try {
51
+ return this.client !== undefined;
52
+ } catch {
53
+ return false;
54
+ }
55
+ },
56
+ };
57
+ private client: GoogleGenerativeAI;
58
+ private defaultModel: string;
59
+ private defaultMaxTokens: number;
60
+
61
+ constructor(options: GoogleProviderOptions) {
62
+ this.client = new GoogleGenerativeAI(options.apiKey);
63
+ this.defaultModel = options.model || DEFAULT_MODEL;
64
+ this.defaultMaxTokens = options.maxTokens || DEFAULT_MAX_TOKENS;
65
+ }
66
+
67
+ async complete(
68
+ messages: ChatMessage[],
69
+ options?: CompletionOptions,
70
+ ): Promise<CompletionResult> {
71
+ const modelName = options?.model || this.defaultModel;
72
+ const model = this.client.getGenerativeModel({
73
+ model: modelName,
74
+ systemInstruction: options?.systemPrompt,
75
+ });
76
+
77
+ const { contents, systemInstruction } = this.prepareMessages(messages, options);
78
+ const genModel = systemInstruction
79
+ ? this.client.getGenerativeModel({ model: modelName, systemInstruction })
80
+ : model;
81
+
82
+ const result = await genModel.generateContent({
83
+ contents,
84
+ generationConfig: {
85
+ maxOutputTokens: options?.maxTokens || this.defaultMaxTokens,
86
+ temperature: options?.temperature,
87
+ },
88
+ });
89
+
90
+ const response = result.response;
91
+ const text = response.text();
92
+ const usage = response.usageMetadata;
93
+
94
+ return {
95
+ content: text,
96
+ usage: {
97
+ inputTokens: usage?.promptTokenCount ?? 0,
98
+ outputTokens: usage?.candidatesTokenCount ?? 0,
99
+ },
100
+ model: modelName,
101
+ finishReason: response.candidates?.[0]?.finishReason ?? 'unknown',
102
+ };
103
+ }
104
+
105
+ async *stream(
106
+ messages: ChatMessage[],
107
+ options?: CompletionOptions,
108
+ ): AsyncGenerator<StreamChunk, void, unknown> {
109
+ const modelName = options?.model || this.defaultModel;
110
+ const { contents, systemInstruction } = this.prepareMessages(messages, options);
111
+ const model = this.client.getGenerativeModel({
112
+ model: modelName,
113
+ systemInstruction: systemInstruction || options?.systemPrompt,
114
+ });
115
+
116
+ try {
117
+ const result = await model.generateContentStream({
118
+ contents,
119
+ generationConfig: {
120
+ maxOutputTokens: options?.maxTokens || this.defaultMaxTokens,
121
+ temperature: options?.temperature,
122
+ },
123
+ });
124
+
125
+ for await (const chunk of result.stream) {
126
+ const text = chunk.text();
127
+ if (text) {
128
+ yield { type: 'text', content: text };
129
+ }
130
+ }
131
+
132
+ const finalResponse = await result.response;
133
+ const usage = finalResponse.usageMetadata;
134
+ yield {
135
+ type: 'done',
136
+ usage: {
137
+ inputTokens: usage?.promptTokenCount ?? 0,
138
+ outputTokens: usage?.candidatesTokenCount ?? 0,
139
+ },
140
+ };
141
+ } catch (error) {
142
+ yield {
143
+ type: 'error',
144
+ error: error instanceof Error ? error.message : 'Unknown error',
145
+ };
146
+ }
147
+ }
148
+
149
+ private prepareMessages(
150
+ messages: ChatMessage[],
151
+ options?: CompletionOptions,
152
+ ): { contents: Array<{ role: string; parts: Array<{ text: string }> }>; systemInstruction?: string } {
153
+ let systemInstruction = options?.systemPrompt;
154
+ const contents: Array<{ role: string; parts: Array<{ text: string }> }> = [];
155
+
156
+ for (const msg of messages) {
157
+ if (msg.role === 'system') {
158
+ systemInstruction = systemInstruction
159
+ ? `${systemInstruction}\n\n${msg.content}`
160
+ : msg.content;
161
+ } else {
162
+ contents.push({
163
+ role: msg.role === 'assistant' ? 'model' : 'user',
164
+ parts: [{ text: msg.content }],
165
+ });
166
+ }
167
+ }
168
+
169
+ // Gemini requires alternating user/model turns, starting with user
170
+ if (contents.length > 0 && contents[0].role !== 'user') {
171
+ contents.unshift({ role: 'user', parts: [{ text: '(Starting conversation)' }] });
172
+ }
173
+
174
+ return { contents, systemInstruction };
175
+ }
176
+ }
package/src/groq.ts ADDED
@@ -0,0 +1,263 @@
1
+ import type {
2
+ Provider,
3
+ ProviderMetadata,
4
+ ChatMessage,
5
+ CompletionOptions,
6
+ CompletionResult,
7
+ StreamChunk,
8
+ } from './types.js';
9
+
10
+ const DEFAULT_MODEL = 'llama-3.3-70b-versatile';
11
+ const DEFAULT_MAX_TOKENS = 4096;
12
+ const BASE_URL = 'https://api.groq.com/openai/v1';
13
+
14
+ export interface GroqProviderOptions {
15
+ apiKey: string;
16
+ model?: string;
17
+ maxTokens?: number;
18
+ }
19
+
20
+ interface GroqChatMessage {
21
+ role: string;
22
+ content: string;
23
+ }
24
+
25
+ interface GroqChoice {
26
+ message: { role: string; content: string };
27
+ finish_reason: string;
28
+ }
29
+
30
+ interface GroqUsage {
31
+ prompt_tokens: number;
32
+ completion_tokens: number;
33
+ }
34
+
35
+ interface GroqChatResponse {
36
+ choices: GroqChoice[];
37
+ usage: GroqUsage;
38
+ model: string;
39
+ }
40
+
41
+ interface GroqStreamDelta {
42
+ content?: string;
43
+ }
44
+
45
+ interface GroqStreamChoice {
46
+ delta: GroqStreamDelta;
47
+ finish_reason: string | null;
48
+ }
49
+
50
+ interface GroqStreamChunk {
51
+ choices: GroqStreamChoice[];
52
+ usage?: GroqUsage;
53
+ }
54
+
55
+ export class GroqProvider implements Provider {
56
+ name = 'groq';
57
+ metadata: ProviderMetadata = {
58
+ name: 'groq',
59
+ displayName: 'Groq',
60
+ models: {
61
+ 'llama-3.3-70b-versatile': {
62
+ maxContextTokens: 128000,
63
+ supportsVision: false,
64
+ supportsTools: true,
65
+ supportsStreaming: true,
66
+ supportsImageGen: false,
67
+ costPer1kInput: 0.00059,
68
+ costPer1kOutput: 0.00079,
69
+ strengths: ['fast', 'reasoning', 'code'],
70
+ isLocal: false,
71
+ },
72
+ 'llama-3.1-8b-instant': {
73
+ maxContextTokens: 131072,
74
+ supportsVision: false,
75
+ supportsTools: true,
76
+ supportsStreaming: true,
77
+ supportsImageGen: false,
78
+ costPer1kInput: 0.00005,
79
+ costPer1kOutput: 0.00008,
80
+ strengths: ['fast', 'low-cost'],
81
+ isLocal: false,
82
+ },
83
+ 'mixtral-8x7b-32768': {
84
+ maxContextTokens: 32768,
85
+ supportsVision: false,
86
+ supportsTools: true,
87
+ supportsStreaming: true,
88
+ supportsImageGen: false,
89
+ costPer1kInput: 0.00024,
90
+ costPer1kOutput: 0.00024,
91
+ strengths: ['fast', 'code', 'multilingual'],
92
+ isLocal: false,
93
+ },
94
+ 'gemma2-9b-it': {
95
+ maxContextTokens: 8192,
96
+ supportsVision: false,
97
+ supportsTools: true,
98
+ supportsStreaming: true,
99
+ supportsImageGen: false,
100
+ costPer1kInput: 0.0002,
101
+ costPer1kOutput: 0.0002,
102
+ strengths: ['fast', 'low-cost'],
103
+ isLocal: false,
104
+ },
105
+ },
106
+ isAvailable: async () => {
107
+ try {
108
+ const response = await fetch(`${BASE_URL}/models`, {
109
+ headers: { Authorization: `Bearer ${this.apiKey}` },
110
+ });
111
+ return response.ok;
112
+ } catch {
113
+ return false;
114
+ }
115
+ },
116
+ };
117
+
118
+ private apiKey: string;
119
+ private defaultModel: string;
120
+ private defaultMaxTokens: number;
121
+
122
+ constructor(options: GroqProviderOptions) {
123
+ this.apiKey = options.apiKey;
124
+ this.defaultModel = options.model || DEFAULT_MODEL;
125
+ this.defaultMaxTokens = options.maxTokens || DEFAULT_MAX_TOKENS;
126
+ }
127
+
128
+ async complete(
129
+ messages: ChatMessage[],
130
+ options?: CompletionOptions,
131
+ ): Promise<CompletionResult> {
132
+ const model = options?.model || this.defaultModel;
133
+ const groqMessages = this.prepareMessages(messages, options);
134
+
135
+ const response = await fetch(`${BASE_URL}/chat/completions`, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'Content-Type': 'application/json',
139
+ Authorization: `Bearer ${this.apiKey}`,
140
+ },
141
+ body: JSON.stringify({
142
+ model,
143
+ max_tokens: options?.maxTokens || this.defaultMaxTokens,
144
+ messages: groqMessages,
145
+ temperature: options?.temperature,
146
+ }),
147
+ });
148
+
149
+ if (!response.ok) {
150
+ throw new Error(`Groq API error: ${response.status} ${response.statusText}`);
151
+ }
152
+
153
+ const data = (await response.json()) as GroqChatResponse;
154
+ const choice = data.choices[0];
155
+
156
+ return {
157
+ content: choice?.message?.content || '',
158
+ usage: {
159
+ inputTokens: data.usage?.prompt_tokens || 0,
160
+ outputTokens: data.usage?.completion_tokens || 0,
161
+ },
162
+ model: data.model,
163
+ finishReason: choice?.finish_reason || 'unknown',
164
+ };
165
+ }
166
+
167
+ async *stream(
168
+ messages: ChatMessage[],
169
+ options?: CompletionOptions,
170
+ ): AsyncGenerator<StreamChunk, void, unknown> {
171
+ const model = options?.model || this.defaultModel;
172
+ const groqMessages = this.prepareMessages(messages, options);
173
+
174
+ try {
175
+ const response = await fetch(`${BASE_URL}/chat/completions`, {
176
+ method: 'POST',
177
+ headers: {
178
+ 'Content-Type': 'application/json',
179
+ Authorization: `Bearer ${this.apiKey}`,
180
+ },
181
+ body: JSON.stringify({
182
+ model,
183
+ max_tokens: options?.maxTokens || this.defaultMaxTokens,
184
+ messages: groqMessages,
185
+ temperature: options?.temperature,
186
+ stream: true,
187
+ stream_options: { include_usage: true },
188
+ }),
189
+ });
190
+
191
+ if (!response.ok) {
192
+ throw new Error(`Groq API error: ${response.status} ${response.statusText}`);
193
+ }
194
+
195
+ const reader = response.body?.getReader();
196
+ if (!reader) {
197
+ throw new Error('No response body');
198
+ }
199
+
200
+ const decoder = new TextDecoder();
201
+ let buffer = '';
202
+ let inputTokens = 0;
203
+ let outputTokens = 0;
204
+
205
+ while (true) {
206
+ const { done, value } = await reader.read();
207
+ if (done) break;
208
+
209
+ buffer += decoder.decode(value, { stream: true });
210
+ const lines = buffer.split('\n');
211
+ buffer = lines.pop() || '';
212
+
213
+ for (const line of lines) {
214
+ const trimmed = line.trim();
215
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
216
+ const data = trimmed.slice(6);
217
+ if (data === '[DONE]') continue;
218
+
219
+ const chunk = JSON.parse(data) as GroqStreamChunk;
220
+
221
+ const delta = chunk.choices[0]?.delta;
222
+ if (delta?.content) {
223
+ yield { type: 'text', content: delta.content };
224
+ }
225
+
226
+ if (chunk.usage) {
227
+ inputTokens = chunk.usage.prompt_tokens || 0;
228
+ outputTokens = chunk.usage.completion_tokens || 0;
229
+ }
230
+
231
+ if (chunk.choices[0]?.finish_reason) {
232
+ yield {
233
+ type: 'done',
234
+ usage: { inputTokens, outputTokens },
235
+ };
236
+ }
237
+ }
238
+ }
239
+ } catch (error) {
240
+ yield {
241
+ type: 'error',
242
+ error: error instanceof Error ? error.message : 'Unknown error',
243
+ };
244
+ }
245
+ }
246
+
247
+ private prepareMessages(
248
+ messages: ChatMessage[],
249
+ options?: CompletionOptions,
250
+ ): GroqChatMessage[] {
251
+ const groqMessages: GroqChatMessage[] = [];
252
+
253
+ if (options?.systemPrompt) {
254
+ groqMessages.push({ role: 'system', content: options.systemPrompt });
255
+ }
256
+
257
+ for (const msg of messages) {
258
+ groqMessages.push({ role: msg.role, content: msg.content });
259
+ }
260
+
261
+ return groqMessages;
262
+ }
263
+ }
package/src/index.ts ADDED
@@ -0,0 +1,44 @@
1
+ export { AnthropicProvider, type AnthropicProviderOptions } from './anthropic.js';
2
+ export { OpenAIProvider, type OpenAIProviderOptions } from './openai.js';
3
+ export { GoogleProvider, type GoogleProviderOptions } from './google.js';
4
+ export { OllamaProvider, type OllamaProviderOptions } from './ollama.js';
5
+ export { OpenAICompatibleProvider, type OpenAICompatibleProviderOptions } from './openai-compatible.js';
6
+ export { GroqProvider, type GroqProviderOptions } from './groq.js';
7
+ export { ReplicateProvider, type ReplicateProviderOptions } from './replicate.js';
8
+ export { DeepSeekProvider, type DeepSeekProviderOptions } from './deepseek.js';
9
+ export { CohereProvider, type CohereProviderOptions } from './cohere.js';
10
+ export { XAIProvider, type XAIProviderOptions } from './xai.js';
11
+ export { ProviderFactory, type ProviderFactoryOptions, type ProviderName } from './factory.js';
12
+ export {
13
+ getAnthropicThinkingBudget,
14
+ getOpenAIReasoningEffort,
15
+ isOpenAIReasoningModel,
16
+ } from './thinking-levels.js';
17
+ export {
18
+ isSetupToken,
19
+ validateSetupToken,
20
+ readClaudeCliCredentials,
21
+ resolveAnthropicApiKey,
22
+ generatePKCE,
23
+ buildAuthorizationUrl,
24
+ exchangeCodeForTokens,
25
+ refreshOAuthToken,
26
+ refreshPKCEOAuthToken,
27
+ writeClaudeCliCredentials,
28
+ type ClaudeOAuthCredentials,
29
+ } from './claude-oauth.js';
30
+ export type {
31
+ Provider,
32
+ ProviderConfig,
33
+ ProviderMetadata,
34
+ ModelCapabilities,
35
+ ChatMessage,
36
+ CompletionOptions,
37
+ CompletionResult,
38
+ StreamChunk,
39
+ MessageRole,
40
+ ThinkingLevel,
41
+ ToolDefinition,
42
+ ToolUse,
43
+ ToolResultMessage,
44
+ } from './types.js';
package/src/ollama.ts ADDED
@@ -0,0 +1,194 @@
1
+ import type {
2
+ Provider,
3
+ ProviderMetadata,
4
+ ChatMessage,
5
+ CompletionOptions,
6
+ CompletionResult,
7
+ StreamChunk,
8
+ } from './types.js';
9
+
10
+ const DEFAULT_BASE_URL = 'http://localhost:11434';
11
+ const DEFAULT_MODEL = 'llama3';
12
+ const DEFAULT_MAX_TOKENS = 4096;
13
+
14
+ export interface OllamaProviderOptions {
15
+ baseUrl?: string;
16
+ model?: string;
17
+ maxTokens?: number;
18
+ }
19
+
20
+ interface OllamaChatMessage {
21
+ role: string;
22
+ content: string;
23
+ }
24
+
25
+ interface OllamaChatResponse {
26
+ message: { role: string; content: string };
27
+ done: boolean;
28
+ eval_count?: number;
29
+ prompt_eval_count?: number;
30
+ }
31
+
32
+ export class OllamaProvider implements Provider {
33
+ name = 'ollama';
34
+ metadata: ProviderMetadata;
35
+ private baseUrl: string;
36
+ private defaultModel: string;
37
+ private defaultMaxTokens: number;
38
+
39
+ constructor(options: OllamaProviderOptions) {
40
+ this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
41
+ this.defaultModel = options.model || DEFAULT_MODEL;
42
+ this.defaultMaxTokens = options.maxTokens || DEFAULT_MAX_TOKENS;
43
+
44
+ this.metadata = {
45
+ name: 'ollama',
46
+ displayName: 'Ollama (Local)',
47
+ models: {
48
+ [this.defaultModel]: {
49
+ maxContextTokens: 8192,
50
+ supportsVision: false,
51
+ supportsTools: false,
52
+ supportsStreaming: true,
53
+ supportsImageGen: false,
54
+ costPer1kInput: 0,
55
+ costPer1kOutput: 0,
56
+ strengths: ['fast', 'private'],
57
+ isLocal: true,
58
+ },
59
+ },
60
+ isAvailable: async () => {
61
+ try {
62
+ const response = await fetch(`${this.baseUrl}/api/tags`);
63
+ return response.ok;
64
+ } catch {
65
+ return false;
66
+ }
67
+ },
68
+ };
69
+ }
70
+
71
+ async complete(
72
+ messages: ChatMessage[],
73
+ options?: CompletionOptions,
74
+ ): Promise<CompletionResult> {
75
+ const model = options?.model || this.defaultModel;
76
+ const ollamaMessages = this.prepareMessages(messages, options);
77
+
78
+ const response = await fetch(`${this.baseUrl}/api/chat`, {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({
82
+ model,
83
+ messages: ollamaMessages,
84
+ stream: false,
85
+ options: {
86
+ num_predict: options?.maxTokens || this.defaultMaxTokens,
87
+ temperature: options?.temperature,
88
+ },
89
+ }),
90
+ });
91
+
92
+ if (!response.ok) {
93
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
94
+ }
95
+
96
+ const data = await response.json() as OllamaChatResponse;
97
+
98
+ return {
99
+ content: data.message.content,
100
+ usage: {
101
+ inputTokens: data.prompt_eval_count ?? 0,
102
+ outputTokens: data.eval_count ?? 0,
103
+ },
104
+ model,
105
+ finishReason: data.done ? 'stop' : 'unknown',
106
+ };
107
+ }
108
+
109
+ async *stream(
110
+ messages: ChatMessage[],
111
+ options?: CompletionOptions,
112
+ ): AsyncGenerator<StreamChunk, void, unknown> {
113
+ const model = options?.model || this.defaultModel;
114
+ const ollamaMessages = this.prepareMessages(messages, options);
115
+
116
+ try {
117
+ const response = await fetch(`${this.baseUrl}/api/chat`, {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({
121
+ model,
122
+ messages: ollamaMessages,
123
+ stream: true,
124
+ options: {
125
+ num_predict: options?.maxTokens || this.defaultMaxTokens,
126
+ temperature: options?.temperature,
127
+ },
128
+ }),
129
+ });
130
+
131
+ if (!response.ok) {
132
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
133
+ }
134
+
135
+ const reader = response.body?.getReader();
136
+ if (!reader) {
137
+ throw new Error('No response body');
138
+ }
139
+
140
+ const decoder = new TextDecoder();
141
+ let buffer = '';
142
+
143
+ while (true) {
144
+ const { done, value } = await reader.read();
145
+ if (done) break;
146
+
147
+ buffer += decoder.decode(value, { stream: true });
148
+ const lines = buffer.split('\n');
149
+ buffer = lines.pop() || '';
150
+
151
+ for (const line of lines) {
152
+ if (!line.trim()) continue;
153
+ const chunk = JSON.parse(line) as OllamaChatResponse;
154
+
155
+ if (chunk.message?.content) {
156
+ yield { type: 'text', content: chunk.message.content };
157
+ }
158
+
159
+ if (chunk.done) {
160
+ yield {
161
+ type: 'done',
162
+ usage: {
163
+ inputTokens: chunk.prompt_eval_count ?? 0,
164
+ outputTokens: chunk.eval_count ?? 0,
165
+ },
166
+ };
167
+ }
168
+ }
169
+ }
170
+ } catch (error) {
171
+ yield {
172
+ type: 'error',
173
+ error: error instanceof Error ? error.message : 'Unknown error',
174
+ };
175
+ }
176
+ }
177
+
178
+ private prepareMessages(
179
+ messages: ChatMessage[],
180
+ options?: CompletionOptions,
181
+ ): OllamaChatMessage[] {
182
+ const ollamaMessages: OllamaChatMessage[] = [];
183
+
184
+ if (options?.systemPrompt) {
185
+ ollamaMessages.push({ role: 'system', content: options.systemPrompt });
186
+ }
187
+
188
+ for (const msg of messages) {
189
+ ollamaMessages.push({ role: msg.role, content: msg.content });
190
+ }
191
+
192
+ return ollamaMessages;
193
+ }
194
+ }