@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.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -0
- package/dist/adapters/providerImageAdapter.d.ts +1 -1
- package/dist/adapters/providerImageAdapter.js +62 -0
- package/dist/agent/directTools.d.ts +0 -72
- package/dist/agent/directTools.js +3 -74
- package/dist/cli/commands/config.d.ts +18 -18
- package/dist/cli/factories/commandFactory.js +1 -0
- package/dist/constants/enums.d.ts +1 -0
- package/dist/constants/enums.js +3 -1
- package/dist/constants/tokens.d.ts +3 -0
- package/dist/constants/tokens.js +3 -0
- package/dist/core/baseProvider.d.ts +56 -53
- package/dist/core/baseProvider.js +107 -1095
- package/dist/core/constants.d.ts +3 -0
- package/dist/core/constants.js +6 -3
- package/dist/core/modelConfiguration.js +10 -0
- package/dist/core/modules/GenerationHandler.d.ts +63 -0
- package/dist/core/modules/GenerationHandler.js +230 -0
- package/dist/core/modules/MessageBuilder.d.ts +39 -0
- package/dist/core/modules/MessageBuilder.js +179 -0
- package/dist/core/modules/StreamHandler.d.ts +52 -0
- package/dist/core/modules/StreamHandler.js +103 -0
- package/dist/core/modules/TelemetryHandler.d.ts +64 -0
- package/dist/core/modules/TelemetryHandler.js +170 -0
- package/dist/core/modules/ToolsManager.d.ts +98 -0
- package/dist/core/modules/ToolsManager.js +521 -0
- package/dist/core/modules/Utilities.d.ts +88 -0
- package/dist/core/modules/Utilities.js +329 -0
- package/dist/factories/providerRegistry.js +1 -1
- package/dist/lib/adapters/providerImageAdapter.d.ts +1 -1
- package/dist/lib/adapters/providerImageAdapter.js +62 -0
- package/dist/lib/agent/directTools.d.ts +0 -72
- package/dist/lib/agent/directTools.js +3 -74
- package/dist/lib/constants/enums.d.ts +1 -0
- package/dist/lib/constants/enums.js +3 -1
- package/dist/lib/constants/tokens.d.ts +3 -0
- package/dist/lib/constants/tokens.js +3 -0
- package/dist/lib/core/baseProvider.d.ts +56 -53
- package/dist/lib/core/baseProvider.js +107 -1095
- package/dist/lib/core/constants.d.ts +3 -0
- package/dist/lib/core/constants.js +6 -3
- package/dist/lib/core/modelConfiguration.js +10 -0
- package/dist/lib/core/modules/GenerationHandler.d.ts +63 -0
- package/dist/lib/core/modules/GenerationHandler.js +231 -0
- package/dist/lib/core/modules/MessageBuilder.d.ts +39 -0
- package/dist/lib/core/modules/MessageBuilder.js +180 -0
- package/dist/lib/core/modules/StreamHandler.d.ts +52 -0
- package/dist/lib/core/modules/StreamHandler.js +104 -0
- package/dist/lib/core/modules/TelemetryHandler.d.ts +64 -0
- package/dist/lib/core/modules/TelemetryHandler.js +171 -0
- package/dist/lib/core/modules/ToolsManager.d.ts +98 -0
- package/dist/lib/core/modules/ToolsManager.js +522 -0
- package/dist/lib/core/modules/Utilities.d.ts +88 -0
- package/dist/lib/core/modules/Utilities.js +330 -0
- package/dist/lib/factories/providerRegistry.js +1 -1
- package/dist/lib/mcp/servers/agent/directToolsServer.js +0 -1
- package/dist/lib/memory/mem0Initializer.d.ts +32 -1
- package/dist/lib/memory/mem0Initializer.js +55 -2
- package/dist/lib/models/modelRegistry.js +44 -0
- package/dist/lib/neurolink.d.ts +1 -1
- package/dist/lib/neurolink.js +43 -10
- package/dist/lib/providers/amazonBedrock.js +59 -10
- package/dist/lib/providers/anthropic.js +2 -30
- package/dist/lib/providers/azureOpenai.js +2 -24
- package/dist/lib/providers/googleAiStudio.js +2 -24
- package/dist/lib/providers/googleVertex.js +2 -45
- package/dist/lib/providers/huggingFace.js +3 -31
- package/dist/lib/providers/litellm.d.ts +1 -1
- package/dist/lib/providers/litellm.js +110 -44
- package/dist/lib/providers/mistral.js +5 -32
- package/dist/lib/providers/ollama.d.ts +1 -0
- package/dist/lib/providers/ollama.js +476 -129
- package/dist/lib/providers/openAI.js +2 -28
- package/dist/lib/providers/openaiCompatible.js +3 -31
- package/dist/lib/types/content.d.ts +16 -113
- package/dist/lib/types/content.js +16 -2
- package/dist/lib/types/conversation.d.ts +3 -17
- package/dist/lib/types/generateTypes.d.ts +2 -2
- package/dist/lib/types/index.d.ts +2 -0
- package/dist/lib/types/index.js +2 -0
- package/dist/lib/types/multimodal.d.ts +282 -0
- package/dist/lib/types/multimodal.js +101 -0
- package/dist/lib/types/streamTypes.d.ts +2 -2
- package/dist/lib/utils/imageProcessor.d.ts +1 -1
- package/dist/lib/utils/messageBuilder.js +25 -2
- package/dist/lib/utils/multimodalOptionsBuilder.d.ts +1 -1
- package/dist/lib/utils/pdfProcessor.d.ts +9 -0
- package/dist/lib/utils/pdfProcessor.js +67 -9
- package/dist/mcp/servers/agent/directToolsServer.js +0 -1
- package/dist/memory/mem0Initializer.d.ts +32 -1
- package/dist/memory/mem0Initializer.js +55 -2
- package/dist/models/modelRegistry.js +44 -0
- package/dist/neurolink.d.ts +1 -1
- package/dist/neurolink.js +43 -10
- package/dist/providers/amazonBedrock.js +59 -10
- package/dist/providers/anthropic.js +2 -30
- package/dist/providers/azureOpenai.js +2 -24
- package/dist/providers/googleAiStudio.js +2 -24
- package/dist/providers/googleVertex.js +2 -45
- package/dist/providers/huggingFace.js +3 -31
- package/dist/providers/litellm.d.ts +1 -1
- package/dist/providers/litellm.js +110 -44
- package/dist/providers/mistral.js +5 -32
- package/dist/providers/ollama.d.ts +1 -0
- package/dist/providers/ollama.js +476 -129
- package/dist/providers/openAI.js +2 -28
- package/dist/providers/openaiCompatible.js +3 -31
- package/dist/types/content.d.ts +16 -113
- package/dist/types/content.js +16 -2
- package/dist/types/conversation.d.ts +3 -17
- package/dist/types/generateTypes.d.ts +2 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/multimodal.d.ts +282 -0
- package/dist/types/multimodal.js +100 -0
- package/dist/types/streamTypes.d.ts +2 -2
- package/dist/utils/imageProcessor.d.ts +1 -1
- package/dist/utils/messageBuilder.js +25 -2
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -1
- package/dist/utils/pdfProcessor.d.ts +9 -0
- package/dist/utils/pdfProcessor.js +67 -9
- package/package.json +5 -2
package/dist/providers/ollama.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
85
|
+
messages,
|
|
86
|
+
temperature: options.temperature,
|
|
87
|
+
max_tokens: options.maxTokens,
|
|
77
88
|
stream: false,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
283
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
logger.debug(`Ollama (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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);
|