@juspay/neurolink 8.3.0 → 8.4.1

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 (123) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -0
  3. package/dist/adapters/providerImageAdapter.d.ts +1 -1
  4. package/dist/adapters/providerImageAdapter.js +62 -0
  5. package/dist/agent/directTools.d.ts +0 -72
  6. package/dist/agent/directTools.js +3 -74
  7. package/dist/cli/commands/config.d.ts +18 -18
  8. package/dist/cli/factories/commandFactory.js +1 -0
  9. package/dist/constants/enums.d.ts +1 -0
  10. package/dist/constants/enums.js +3 -1
  11. package/dist/constants/tokens.d.ts +3 -0
  12. package/dist/constants/tokens.js +3 -0
  13. package/dist/core/baseProvider.d.ts +56 -53
  14. package/dist/core/baseProvider.js +107 -1095
  15. package/dist/core/constants.d.ts +3 -0
  16. package/dist/core/constants.js +6 -3
  17. package/dist/core/modelConfiguration.js +10 -0
  18. package/dist/core/modules/GenerationHandler.d.ts +63 -0
  19. package/dist/core/modules/GenerationHandler.js +230 -0
  20. package/dist/core/modules/MessageBuilder.d.ts +39 -0
  21. package/dist/core/modules/MessageBuilder.js +179 -0
  22. package/dist/core/modules/StreamHandler.d.ts +52 -0
  23. package/dist/core/modules/StreamHandler.js +103 -0
  24. package/dist/core/modules/TelemetryHandler.d.ts +64 -0
  25. package/dist/core/modules/TelemetryHandler.js +170 -0
  26. package/dist/core/modules/ToolsManager.d.ts +98 -0
  27. package/dist/core/modules/ToolsManager.js +521 -0
  28. package/dist/core/modules/Utilities.d.ts +88 -0
  29. package/dist/core/modules/Utilities.js +329 -0
  30. package/dist/factories/providerRegistry.js +1 -1
  31. package/dist/lib/adapters/providerImageAdapter.d.ts +1 -1
  32. package/dist/lib/adapters/providerImageAdapter.js +62 -0
  33. package/dist/lib/agent/directTools.d.ts +0 -72
  34. package/dist/lib/agent/directTools.js +3 -74
  35. package/dist/lib/constants/enums.d.ts +1 -0
  36. package/dist/lib/constants/enums.js +3 -1
  37. package/dist/lib/constants/tokens.d.ts +3 -0
  38. package/dist/lib/constants/tokens.js +3 -0
  39. package/dist/lib/core/baseProvider.d.ts +56 -53
  40. package/dist/lib/core/baseProvider.js +107 -1095
  41. package/dist/lib/core/constants.d.ts +3 -0
  42. package/dist/lib/core/constants.js +6 -3
  43. package/dist/lib/core/modelConfiguration.js +10 -0
  44. package/dist/lib/core/modules/GenerationHandler.d.ts +63 -0
  45. package/dist/lib/core/modules/GenerationHandler.js +231 -0
  46. package/dist/lib/core/modules/MessageBuilder.d.ts +39 -0
  47. package/dist/lib/core/modules/MessageBuilder.js +180 -0
  48. package/dist/lib/core/modules/StreamHandler.d.ts +52 -0
  49. package/dist/lib/core/modules/StreamHandler.js +104 -0
  50. package/dist/lib/core/modules/TelemetryHandler.d.ts +64 -0
  51. package/dist/lib/core/modules/TelemetryHandler.js +171 -0
  52. package/dist/lib/core/modules/ToolsManager.d.ts +98 -0
  53. package/dist/lib/core/modules/ToolsManager.js +522 -0
  54. package/dist/lib/core/modules/Utilities.d.ts +88 -0
  55. package/dist/lib/core/modules/Utilities.js +330 -0
  56. package/dist/lib/factories/providerRegistry.js +1 -1
  57. package/dist/lib/mcp/servers/agent/directToolsServer.js +0 -1
  58. package/dist/lib/memory/mem0Initializer.d.ts +32 -1
  59. package/dist/lib/memory/mem0Initializer.js +55 -2
  60. package/dist/lib/models/modelRegistry.js +44 -0
  61. package/dist/lib/neurolink.d.ts +1 -1
  62. package/dist/lib/neurolink.js +43 -10
  63. package/dist/lib/providers/amazonBedrock.js +59 -10
  64. package/dist/lib/providers/anthropic.js +2 -30
  65. package/dist/lib/providers/azureOpenai.js +2 -24
  66. package/dist/lib/providers/googleAiStudio.js +2 -24
  67. package/dist/lib/providers/googleVertex.js +2 -45
  68. package/dist/lib/providers/huggingFace.js +3 -31
  69. package/dist/lib/providers/litellm.d.ts +1 -1
  70. package/dist/lib/providers/litellm.js +110 -44
  71. package/dist/lib/providers/mistral.js +5 -32
  72. package/dist/lib/providers/ollama.d.ts +1 -0
  73. package/dist/lib/providers/ollama.js +476 -129
  74. package/dist/lib/providers/openAI.js +2 -28
  75. package/dist/lib/providers/openaiCompatible.js +3 -31
  76. package/dist/lib/types/content.d.ts +16 -113
  77. package/dist/lib/types/content.js +16 -2
  78. package/dist/lib/types/conversation.d.ts +3 -17
  79. package/dist/lib/types/generateTypes.d.ts +2 -2
  80. package/dist/lib/types/index.d.ts +2 -0
  81. package/dist/lib/types/index.js +2 -0
  82. package/dist/lib/types/multimodal.d.ts +282 -0
  83. package/dist/lib/types/multimodal.js +101 -0
  84. package/dist/lib/types/streamTypes.d.ts +2 -2
  85. package/dist/lib/utils/imageProcessor.d.ts +1 -1
  86. package/dist/lib/utils/messageBuilder.js +25 -2
  87. package/dist/lib/utils/multimodalOptionsBuilder.d.ts +1 -1
  88. package/dist/lib/utils/pdfProcessor.d.ts +9 -0
  89. package/dist/lib/utils/pdfProcessor.js +67 -9
  90. package/dist/mcp/servers/agent/directToolsServer.js +0 -1
  91. package/dist/memory/mem0Initializer.d.ts +32 -1
  92. package/dist/memory/mem0Initializer.js +55 -2
  93. package/dist/models/modelRegistry.js +44 -0
  94. package/dist/neurolink.d.ts +1 -1
  95. package/dist/neurolink.js +43 -10
  96. package/dist/providers/amazonBedrock.js +59 -10
  97. package/dist/providers/anthropic.js +2 -30
  98. package/dist/providers/azureOpenai.js +2 -24
  99. package/dist/providers/googleAiStudio.js +2 -24
  100. package/dist/providers/googleVertex.js +2 -45
  101. package/dist/providers/huggingFace.js +3 -31
  102. package/dist/providers/litellm.d.ts +1 -1
  103. package/dist/providers/litellm.js +110 -44
  104. package/dist/providers/mistral.js +5 -32
  105. package/dist/providers/ollama.d.ts +1 -0
  106. package/dist/providers/ollama.js +476 -129
  107. package/dist/providers/openAI.js +2 -28
  108. package/dist/providers/openaiCompatible.js +3 -31
  109. package/dist/types/content.d.ts +16 -113
  110. package/dist/types/content.js +16 -2
  111. package/dist/types/conversation.d.ts +3 -17
  112. package/dist/types/generateTypes.d.ts +2 -2
  113. package/dist/types/index.d.ts +2 -0
  114. package/dist/types/index.js +2 -0
  115. package/dist/types/multimodal.d.ts +282 -0
  116. package/dist/types/multimodal.js +100 -0
  117. package/dist/types/streamTypes.d.ts +2 -2
  118. package/dist/utils/imageProcessor.d.ts +1 -1
  119. package/dist/utils/messageBuilder.js +25 -2
  120. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -1
  121. package/dist/utils/pdfProcessor.d.ts +9 -0
  122. package/dist/utils/pdfProcessor.js +67 -9
  123. package/package.json +5 -2
@@ -15,6 +15,11 @@ const FALLBACK_OLLAMA_MODEL = "llama3.2:latest"; // Used when primary model fail
15
15
  const getOllamaBaseUrl = () => {
16
16
  return process.env.OLLAMA_BASE_URL || "http://localhost:11434";
17
17
  };
18
+ const isOpenAICompatibleMode = () => {
19
+ // Enable OpenAI-compatible API mode (/v1/chat/completions) instead of native Ollama API (/api/generate)
20
+ // Useful for Ollama deployments that only support OpenAI-compatible routes (e.g., breezehq.dev)
21
+ return process.env.OLLAMA_OPENAI_COMPATIBLE === "true";
22
+ };
18
23
  // Create AbortController with timeout for better compatibility
19
24
  const createAbortSignalWithTimeout = (timeoutMs) => {
20
25
  const controller = new AbortController();
@@ -29,7 +34,9 @@ const getDefaultOllamaModel = () => {
29
34
  return process.env.OLLAMA_MODEL || DEFAULT_OLLAMA_MODEL;
30
35
  };
31
36
  const getOllamaTimeout = () => {
32
- return parseInt(process.env.OLLAMA_TIMEOUT || "60000", 10);
37
+ // Increased default timeout to 240000ms (4 minutes) to support slower native API responses
38
+ // especially for larger models like aliafshar/gemma3-it-qat-tools:latest (12.2B parameters)
39
+ return parseInt(process.env.OLLAMA_TIMEOUT || "240000", 10);
33
40
  };
34
41
  // Create proxy-aware fetch instance
35
42
  const proxyFetch = createProxyFetch();
@@ -62,63 +69,176 @@ class OllamaLanguageModel {
62
69
  .join("\n");
63
70
  }
64
71
  async doGenerate(options) {
72
+ // Vercel AI SDK passes messages via options.messages (same as stream mode)
73
+ // Check options.messages first, then fall back to options.prompt for backward compatibility
65
74
  const messages = options
66
- .messages || [];
67
- const prompt = this.convertMessagesToPrompt(messages);
68
- // Debug: Log what's being sent to Ollama
69
- logger.debug("[OllamaLanguageModel] Messages:", JSON.stringify(messages, null, 2));
70
- logger.debug("[OllamaLanguageModel] Converted Prompt:", JSON.stringify(prompt));
71
- const response = await proxyFetch(`${this.baseUrl}/api/generate`, {
72
- method: "POST",
73
- headers: { "Content-Type": "application/json" },
74
- body: JSON.stringify({
75
+ .messages ||
76
+ options
77
+ .prompt ||
78
+ [];
79
+ // Check if we should use OpenAI-compatible API
80
+ const useOpenAIMode = isOpenAICompatibleMode();
81
+ if (useOpenAIMode) {
82
+ // OpenAI-compatible mode: Use /v1/chat/completions
83
+ const requestBody = {
75
84
  model: this.modelId,
76
- prompt,
85
+ messages,
86
+ temperature: options.temperature,
87
+ max_tokens: options.maxTokens,
77
88
  stream: false,
78
- system: messages.find((m) => m.role === "system")?.content,
79
- options: {
80
- temperature: options.temperature,
81
- num_predict: options.maxTokens,
89
+ };
90
+ logger.debug("[OllamaLanguageModel] Using OpenAI-compatible API with messages:", JSON.stringify(messages, null, 2));
91
+ const response = await proxyFetch(`${this.baseUrl}/v1/chat/completions`, {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify(requestBody),
95
+ signal: createAbortSignalWithTimeout(this.timeout),
96
+ });
97
+ if (!response.ok) {
98
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
99
+ }
100
+ const data = await response.json();
101
+ logger.debug("[OllamaLanguageModel] OpenAI API Response:", JSON.stringify(data, null, 2));
102
+ const text = data.choices?.[0]?.message?.content || "";
103
+ const usage = data.usage || {};
104
+ return {
105
+ text,
106
+ usage: {
107
+ promptTokens: usage.prompt_tokens ??
108
+ this.estimateTokens(JSON.stringify(messages)),
109
+ completionTokens: usage.completion_tokens ?? this.estimateTokens(text),
110
+ totalTokens: usage.total_tokens,
82
111
  },
83
- }),
84
- signal: createAbortSignalWithTimeout(this.timeout),
85
- });
86
- if (!response.ok) {
87
- throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
112
+ finishReason: "stop",
113
+ rawCall: {
114
+ rawPrompt: messages,
115
+ rawSettings: {
116
+ model: this.modelId,
117
+ temperature: options.temperature,
118
+ max_tokens: options.maxTokens,
119
+ },
120
+ },
121
+ rawResponse: {
122
+ headers: {},
123
+ },
124
+ };
88
125
  }
89
- const data = await response.json();
90
- // Debug: Log Ollama API response to understand empty content issue
91
- logger.debug("[OllamaLanguageModel] API Response:", JSON.stringify(data, null, 2));
92
- return {
93
- text: data.response,
94
- usage: {
95
- promptTokens: data.prompt_eval_count ?? this.estimateTokens(prompt),
96
- completionTokens: data.eval_count ?? this.estimateTokens(String(data.response ?? "")),
97
- totalTokens: (data.prompt_eval_count ?? this.estimateTokens(prompt)) +
98
- (data.eval_count ?? this.estimateTokens(String(data.response ?? ""))),
99
- },
100
- finishReason: "stop",
101
- rawCall: {
102
- rawPrompt: prompt,
103
- rawSettings: {
126
+ else {
127
+ // Native Ollama mode: Use /api/generate
128
+ const prompt = this.convertMessagesToPrompt(messages);
129
+ logger.debug("[OllamaLanguageModel] Using native API with prompt:", JSON.stringify(prompt));
130
+ const response = await proxyFetch(`${this.baseUrl}/api/generate`, {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify({
104
134
  model: this.modelId,
105
- temperature: options.temperature,
106
- num_predict: options.maxTokens,
135
+ prompt,
136
+ stream: false,
137
+ system: messages.find((m) => m.role === "system")?.content,
138
+ options: {
139
+ temperature: options.temperature,
140
+ num_predict: options.maxTokens,
141
+ },
142
+ }),
143
+ signal: createAbortSignalWithTimeout(this.timeout),
144
+ });
145
+ if (!response.ok) {
146
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
147
+ }
148
+ const data = await response.json();
149
+ logger.debug("[OllamaLanguageModel] Native API Response:", JSON.stringify(data, null, 2));
150
+ return {
151
+ text: data.response,
152
+ usage: {
153
+ promptTokens: data.prompt_eval_count ?? this.estimateTokens(prompt),
154
+ completionTokens: data.eval_count ?? this.estimateTokens(String(data.response ?? "")),
155
+ totalTokens: (data.prompt_eval_count ?? this.estimateTokens(prompt)) +
156
+ (data.eval_count ??
157
+ this.estimateTokens(String(data.response ?? ""))),
107
158
  },
108
- },
109
- rawResponse: {
110
- headers: {},
111
- },
112
- };
159
+ finishReason: "stop",
160
+ rawCall: {
161
+ rawPrompt: prompt,
162
+ rawSettings: {
163
+ model: this.modelId,
164
+ temperature: options.temperature,
165
+ num_predict: options.maxTokens,
166
+ },
167
+ },
168
+ rawResponse: {
169
+ headers: {},
170
+ },
171
+ };
172
+ }
113
173
  }
114
174
  async doStream(options) {
115
175
  const messages = options
116
176
  .messages || [];
117
- const prompt = this.convertMessagesToPrompt(messages);
118
- const response = await proxyFetch(`${this.baseUrl}/api/generate`, {
119
- method: "POST",
120
- headers: { "Content-Type": "application/json" },
121
- body: JSON.stringify({
177
+ // Check if we should use OpenAI-compatible API
178
+ const useOpenAIMode = isOpenAICompatibleMode();
179
+ if (useOpenAIMode) {
180
+ // OpenAI-compatible mode: Use /v1/chat/completions
181
+ const requestUrl = `${this.baseUrl}/v1/chat/completions`;
182
+ const requestBody = {
183
+ model: this.modelId,
184
+ messages,
185
+ temperature: options.temperature,
186
+ max_tokens: options.maxTokens,
187
+ stream: true,
188
+ };
189
+ logger.debug("[OllamaLanguageModel] doStream: Using OpenAI-compatible API", {
190
+ url: requestUrl,
191
+ baseUrl: this.baseUrl,
192
+ modelId: this.modelId,
193
+ requestBody: JSON.stringify(requestBody),
194
+ });
195
+ const response = await proxyFetch(requestUrl, {
196
+ method: "POST",
197
+ headers: { "Content-Type": "application/json" },
198
+ body: JSON.stringify(requestBody),
199
+ signal: createAbortSignalWithTimeout(this.timeout),
200
+ });
201
+ logger.debug("[OllamaLanguageModel] doStream: Response received", {
202
+ status: response.status,
203
+ statusText: response.statusText,
204
+ ok: response.ok,
205
+ });
206
+ if (!response.ok) {
207
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
208
+ }
209
+ const self = this;
210
+ return {
211
+ stream: new ReadableStream({
212
+ async start(controller) {
213
+ try {
214
+ for await (const chunk of self.parseOpenAIStreamResponse(response, messages)) {
215
+ controller.enqueue(chunk);
216
+ }
217
+ controller.close();
218
+ }
219
+ catch (error) {
220
+ controller.error(error);
221
+ }
222
+ },
223
+ }),
224
+ rawCall: {
225
+ rawPrompt: messages,
226
+ rawSettings: {
227
+ model: this.modelId,
228
+ temperature: options.temperature,
229
+ max_tokens: options.maxTokens,
230
+ },
231
+ },
232
+ rawResponse: {
233
+ headers: {},
234
+ },
235
+ };
236
+ }
237
+ else {
238
+ // Native Ollama mode: Use /api/generate
239
+ const prompt = this.convertMessagesToPrompt(messages);
240
+ const requestUrl = `${this.baseUrl}/api/generate`;
241
+ const requestBody = {
122
242
  model: this.modelId,
123
243
  prompt,
124
244
  stream: true,
@@ -127,39 +247,55 @@ class OllamaLanguageModel {
127
247
  temperature: options.temperature,
128
248
  num_predict: options.maxTokens,
129
249
  },
130
- }),
131
- signal: createAbortSignalWithTimeout(this.timeout),
132
- });
133
- if (!response.ok) {
134
- throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
135
- }
136
- const self = this;
137
- return {
138
- stream: new ReadableStream({
139
- async start(controller) {
140
- try {
141
- for await (const chunk of self.parseStreamResponse(response)) {
142
- controller.enqueue(chunk);
250
+ };
251
+ logger.debug("[OllamaLanguageModel] doStream: Using native API", {
252
+ url: requestUrl,
253
+ baseUrl: this.baseUrl,
254
+ modelId: this.modelId,
255
+ requestBody: JSON.stringify(requestBody),
256
+ });
257
+ const response = await proxyFetch(requestUrl, {
258
+ method: "POST",
259
+ headers: { "Content-Type": "application/json" },
260
+ body: JSON.stringify(requestBody),
261
+ signal: createAbortSignalWithTimeout(this.timeout),
262
+ });
263
+ logger.debug("[OllamaLanguageModel] doStream: Response received", {
264
+ status: response.status,
265
+ statusText: response.statusText,
266
+ ok: response.ok,
267
+ });
268
+ if (!response.ok) {
269
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
270
+ }
271
+ const self = this;
272
+ return {
273
+ stream: new ReadableStream({
274
+ async start(controller) {
275
+ try {
276
+ for await (const chunk of self.parseStreamResponse(response)) {
277
+ controller.enqueue(chunk);
278
+ }
279
+ controller.close();
143
280
  }
144
- controller.close();
145
- }
146
- catch (error) {
147
- controller.error(error);
148
- }
281
+ catch (error) {
282
+ controller.error(error);
283
+ }
284
+ },
285
+ }),
286
+ rawCall: {
287
+ rawPrompt: messages,
288
+ rawSettings: {
289
+ model: this.modelId,
290
+ temperature: options.temperature,
291
+ num_predict: options.maxTokens,
292
+ },
149
293
  },
150
- }),
151
- rawCall: {
152
- rawPrompt: prompt,
153
- rawSettings: {
154
- model: this.modelId,
155
- temperature: options.temperature,
156
- num_predict: options.maxTokens,
294
+ rawResponse: {
295
+ headers: {},
157
296
  },
158
- },
159
- rawResponse: {
160
- headers: {},
161
- },
162
- };
297
+ };
298
+ }
163
299
  }
164
300
  async *parseStreamResponse(response) {
165
301
  const reader = response.body?.getReader();
@@ -213,6 +349,83 @@ class OllamaLanguageModel {
213
349
  reader.releaseLock();
214
350
  }
215
351
  }
352
+ async *parseOpenAIStreamResponse(response, messages) {
353
+ const reader = response.body?.getReader();
354
+ if (!reader) {
355
+ throw new Error("No response body");
356
+ }
357
+ const decoder = new TextDecoder();
358
+ let buffer = "";
359
+ // Estimate prompt tokens from messages (matches non-streaming behavior)
360
+ const totalPromptTokens = this.estimateTokens(JSON.stringify(messages));
361
+ let totalCompletionTokens = 0;
362
+ try {
363
+ while (true) {
364
+ const { done, value } = await reader.read();
365
+ if (done) {
366
+ break;
367
+ }
368
+ buffer += decoder.decode(value, { stream: true });
369
+ const lines = buffer.split("\n");
370
+ buffer = lines.pop() || "";
371
+ for (const line of lines) {
372
+ const trimmed = line.trim();
373
+ if (trimmed === "" || trimmed === "data: [DONE]") {
374
+ continue;
375
+ }
376
+ if (trimmed.startsWith("data: ")) {
377
+ try {
378
+ const jsonStr = trimmed.slice(6); // Remove "data: " prefix
379
+ const data = JSON.parse(jsonStr);
380
+ // Extract content delta
381
+ const content = data.choices?.[0]?.delta?.content;
382
+ if (content) {
383
+ yield {
384
+ type: "text-delta",
385
+ textDelta: content,
386
+ };
387
+ totalCompletionTokens += this.estimateTokens(content);
388
+ }
389
+ // Check for finish
390
+ const finishReason = data.choices?.[0]?.finish_reason;
391
+ if (finishReason === "stop") {
392
+ // Extract usage if available and update tokens
393
+ const promptTokens = data.usage?.prompt_tokens || totalPromptTokens;
394
+ const completionTokens = data.usage?.completion_tokens || totalCompletionTokens;
395
+ yield {
396
+ type: "finish",
397
+ finishReason: "stop",
398
+ usage: {
399
+ promptTokens,
400
+ completionTokens,
401
+ },
402
+ };
403
+ return;
404
+ }
405
+ }
406
+ catch (error) {
407
+ logger.error("Error parsing OpenAI stream response", {
408
+ error,
409
+ line: trimmed,
410
+ });
411
+ }
412
+ }
413
+ }
414
+ }
415
+ // If loop exits without explicit finish, yield final finish
416
+ yield {
417
+ type: "finish",
418
+ finishReason: "stop",
419
+ usage: {
420
+ promptTokens: totalPromptTokens,
421
+ completionTokens: totalCompletionTokens,
422
+ },
423
+ };
424
+ }
425
+ finally {
426
+ reader.releaseLock();
427
+ }
428
+ }
216
429
  }
217
430
  /**
218
431
  * Ollama Provider v2 - BaseProvider Implementation
@@ -279,18 +492,28 @@ export class OllamaProvider extends BaseProvider {
279
492
  // Get tool-capable models from configuration
280
493
  const ollamaConfig = modelConfig.getProviderConfiguration("ollama");
281
494
  const toolCapableModels = ollamaConfig?.modelBehavior?.toolCapableModels || [];
282
- // Check if current model matches tool-capable model patterns
283
- const isToolCapable = toolCapableModels.some((capableModel) => modelName.includes(capableModel));
495
+ // Only disable tools if we have positive evidence the model doesn't support them
496
+ // If toolCapableModels config is empty, assume tools are supported (don't make assumptions)
497
+ if (toolCapableModels.length === 0) {
498
+ logger.debug("Ollama tool calling enabled", {
499
+ model: this.modelName,
500
+ reason: "No tool-capable config defined, assuming tools supported",
501
+ baseUrl: this.baseUrl,
502
+ });
503
+ return true;
504
+ }
505
+ // Config exists - check if current model matches tool-capable model patterns
506
+ const isToolCapable = toolCapableModels.some((capableModel) => modelName.includes(capableModel.toLowerCase()));
284
507
  if (isToolCapable) {
285
508
  logger.debug("Ollama tool calling enabled", {
286
509
  model: this.modelName,
287
- reason: "Model supports function calling",
510
+ reason: "Model in tool-capable list",
288
511
  baseUrl: this.baseUrl,
289
512
  configuredModels: toolCapableModels.length,
290
513
  });
291
514
  return true;
292
515
  }
293
- // Log why tools are disabled for transparency
516
+ // Config exists and model is NOT in list - disable tools
294
517
  logger.debug("Ollama tool calling disabled", {
295
518
  model: this.modelName,
296
519
  reason: "Model not in tool-capable list",
@@ -536,57 +759,134 @@ export class OllamaProvider extends BaseProvider {
536
759
  options.input?.content?.length ||
537
760
  options.input?.files?.length ||
538
761
  options.input?.csvFiles?.length);
539
- let prompt = options.input.text;
540
- let images;
541
- if (hasMultimodalInput) {
542
- logger.debug(`Ollama (generate API): Detected multimodal input`, {
543
- hasImages: !!options.input?.images?.length,
544
- imageCount: options.input?.images?.length || 0,
545
- });
546
- const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
547
- const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
548
- // Extract text from messages for prompt
549
- prompt = multimodalMessages
550
- .map((msg) => (typeof msg.content === "string" ? msg.content : ""))
551
- .join("\n");
552
- // Extract images
553
- images = this.extractImagesFromMessages(multimodalMessages);
554
- }
555
- const requestBody = {
556
- model: this.modelName || FALLBACK_OLLAMA_MODEL,
557
- prompt,
558
- system: options.systemPrompt,
559
- stream: true,
560
- options: {
762
+ const useOpenAIMode = isOpenAICompatibleMode();
763
+ if (useOpenAIMode) {
764
+ // OpenAI-compatible mode: Use /v1/chat/completions with messages
765
+ logger.debug(`Ollama (OpenAI mode): Building messages for streaming`);
766
+ const messages = [];
767
+ if (options.systemPrompt) {
768
+ messages.push({ role: "system", content: options.systemPrompt });
769
+ }
770
+ if (hasMultimodalInput) {
771
+ const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
772
+ const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
773
+ // Convert multimodal messages to text (OpenAI-compatible mode doesn't support images in /v1/chat/completions for Ollama)
774
+ const content = multimodalMessages
775
+ .map((msg) => (typeof msg.content === "string" ? msg.content : ""))
776
+ .join("\n");
777
+ messages.push({ role: "user", content });
778
+ }
779
+ else {
780
+ messages.push({ role: "user", content: options.input.text });
781
+ }
782
+ const requestUrl = `${this.baseUrl}/v1/chat/completions`;
783
+ const requestBody = {
784
+ model: this.modelName || FALLBACK_OLLAMA_MODEL,
785
+ messages,
561
786
  temperature: options.temperature,
562
- num_predict: options.maxTokens,
563
- },
564
- };
565
- if (images && images.length > 0) {
566
- requestBody.images = images;
567
- }
568
- const response = await proxyFetch(`${this.baseUrl}/api/generate`, {
569
- method: "POST",
570
- headers: { "Content-Type": "application/json" },
571
- body: JSON.stringify(requestBody),
572
- signal: createAbortSignalWithTimeout(this.timeout),
573
- });
574
- if (!response.ok) {
575
- throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
787
+ max_tokens: options.maxTokens,
788
+ stream: true,
789
+ };
790
+ logger.debug(`[Ollama OpenAI Mode] About to fetch:`, {
791
+ url: requestUrl,
792
+ baseUrl: this.baseUrl,
793
+ modelName: this.modelName,
794
+ requestBody: JSON.stringify(requestBody),
795
+ });
796
+ const response = await proxyFetch(requestUrl, {
797
+ method: "POST",
798
+ headers: { "Content-Type": "application/json" },
799
+ body: JSON.stringify(requestBody),
800
+ signal: createAbortSignalWithTimeout(this.timeout),
801
+ });
802
+ logger.debug(`[Ollama OpenAI Mode] Response received:`, {
803
+ status: response.status,
804
+ statusText: response.statusText,
805
+ ok: response.ok,
806
+ });
807
+ if (!response.ok) {
808
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
809
+ }
810
+ // Transform to async generator for OpenAI-compatible format
811
+ const self = this;
812
+ const transformedStream = async function* () {
813
+ const generator = self.createOpenAIStream(response);
814
+ for await (const chunk of generator) {
815
+ yield chunk;
816
+ }
817
+ };
818
+ return {
819
+ stream: transformedStream(),
820
+ provider: self.providerName,
821
+ model: self.modelName,
822
+ };
576
823
  }
577
- // Transform to async generator to match other providers
578
- const self = this;
579
- const transformedStream = async function* () {
580
- const generator = self.createOllamaStream(response);
581
- for await (const chunk of generator) {
582
- yield chunk;
824
+ else {
825
+ // Native Ollama mode: Use /api/generate
826
+ let prompt = options.input.text;
827
+ let images;
828
+ if (hasMultimodalInput) {
829
+ logger.debug(`Ollama (native mode): Detected multimodal input`, {
830
+ hasImages: !!options.input?.images?.length,
831
+ imageCount: options.input?.images?.length || 0,
832
+ });
833
+ const multimodalOptions = buildMultimodalOptions(options, this.providerName, this.modelName);
834
+ const multimodalMessages = await buildMultimodalMessagesArray(multimodalOptions, this.providerName, this.modelName);
835
+ // Extract text from messages for prompt
836
+ prompt = multimodalMessages
837
+ .map((msg) => (typeof msg.content === "string" ? msg.content : ""))
838
+ .join("\n");
839
+ // Extract images
840
+ images = this.extractImagesFromMessages(multimodalMessages);
583
841
  }
584
- };
585
- return {
586
- stream: transformedStream(),
587
- provider: this.providerName,
588
- model: this.modelName,
589
- };
842
+ const requestBody = {
843
+ model: this.modelName || FALLBACK_OLLAMA_MODEL,
844
+ prompt,
845
+ system: options.systemPrompt,
846
+ stream: true,
847
+ options: {
848
+ temperature: options.temperature,
849
+ num_predict: options.maxTokens,
850
+ },
851
+ };
852
+ if (images && images.length > 0) {
853
+ requestBody.images = images;
854
+ }
855
+ const requestUrl = `${this.baseUrl}/api/generate`;
856
+ logger.debug(`[Ollama Native Mode] About to fetch:`, {
857
+ url: requestUrl,
858
+ baseUrl: this.baseUrl,
859
+ modelName: this.modelName,
860
+ requestBody: JSON.stringify(requestBody),
861
+ });
862
+ const response = await proxyFetch(requestUrl, {
863
+ method: "POST",
864
+ headers: { "Content-Type": "application/json" },
865
+ body: JSON.stringify(requestBody),
866
+ signal: createAbortSignalWithTimeout(this.timeout),
867
+ });
868
+ logger.debug(`[Ollama Native Mode] Response received:`, {
869
+ status: response.status,
870
+ statusText: response.statusText,
871
+ ok: response.ok,
872
+ });
873
+ if (!response.ok) {
874
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
875
+ }
876
+ // Transform to async generator to match other providers
877
+ const self = this;
878
+ const transformedStream = async function* () {
879
+ const generator = self.createOllamaStream(response);
880
+ for await (const chunk of generator) {
881
+ yield chunk;
882
+ }
883
+ };
884
+ return {
885
+ stream: transformedStream(),
886
+ provider: this.providerName,
887
+ model: this.modelName,
888
+ };
889
+ }
590
890
  }
591
891
  /**
592
892
  * Convert AI SDK tools format to Ollama's function calling format
@@ -1051,6 +1351,53 @@ export class OllamaProvider extends BaseProvider {
1051
1351
  reader.releaseLock();
1052
1352
  }
1053
1353
  }
1354
+ async *createOpenAIStream(response) {
1355
+ const reader = response.body?.getReader();
1356
+ if (!reader) {
1357
+ throw new Error("No response body");
1358
+ }
1359
+ const decoder = new TextDecoder();
1360
+ let buffer = "";
1361
+ try {
1362
+ while (true) {
1363
+ const { done, value } = await reader.read();
1364
+ if (done) {
1365
+ break;
1366
+ }
1367
+ buffer += decoder.decode(value, { stream: true });
1368
+ const lines = buffer.split("\n");
1369
+ buffer = lines.pop() || "";
1370
+ for (const line of lines) {
1371
+ const trimmedLine = line.trim();
1372
+ if (!trimmedLine || trimmedLine === "data: [DONE]") {
1373
+ continue;
1374
+ }
1375
+ if (trimmedLine.startsWith("data: ")) {
1376
+ try {
1377
+ const jsonStr = trimmedLine.slice(6); // Remove "data: " prefix
1378
+ const data = JSON.parse(jsonStr);
1379
+ const content = data.choices?.[0]?.delta?.content;
1380
+ if (content) {
1381
+ yield { content };
1382
+ }
1383
+ if (data.choices?.[0]?.finish_reason) {
1384
+ return;
1385
+ }
1386
+ }
1387
+ catch (error) {
1388
+ logger.error("Error parsing OpenAI stream response", {
1389
+ error,
1390
+ line: trimmedLine,
1391
+ });
1392
+ }
1393
+ }
1394
+ }
1395
+ }
1396
+ }
1397
+ finally {
1398
+ reader.releaseLock();
1399
+ }
1400
+ }
1054
1401
  handleProviderError(error) {
1055
1402
  if (error.name === "TimeoutError") {
1056
1403
  return new TimeoutError(`Ollama request timed out. The model might be loading or the request is too complex.`, this.defaultTimeout);