@juspay/neurolink 9.42.0 → 9.43.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.
- package/CHANGELOG.md +8 -0
- package/dist/auth/anthropicOAuth.js +12 -0
- package/dist/browser/neurolink.min.js +335 -334
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +200 -184
- package/dist/cli/commands/proxy.js +560 -518
- package/dist/core/baseProvider.d.ts +6 -1
- package/dist/core/baseProvider.js +219 -232
- package/dist/core/factory.d.ts +3 -0
- package/dist/core/factory.js +140 -190
- package/dist/core/modules/ToolsManager.d.ts +1 -0
- package/dist/core/modules/ToolsManager.js +40 -42
- package/dist/core/toolEvents.d.ts +3 -0
- package/dist/core/toolEvents.js +7 -0
- package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/evaluation/scorers/scorerRegistry.js +356 -284
- package/dist/lib/auth/anthropicOAuth.js +12 -0
- package/dist/lib/core/baseProvider.d.ts +6 -1
- package/dist/lib/core/baseProvider.js +219 -232
- package/dist/lib/core/factory.d.ts +3 -0
- package/dist/lib/core/factory.js +140 -190
- package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
- package/dist/lib/core/modules/ToolsManager.js +40 -42
- package/dist/lib/core/toolEvents.d.ts +3 -0
- package/dist/lib/core/toolEvents.js +8 -0
- package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/lib/evaluation/scorers/scorerRegistry.js +356 -284
- package/dist/lib/mcp/toolRegistry.d.ts +2 -0
- package/dist/lib/mcp/toolRegistry.js +32 -31
- package/dist/lib/neurolink.d.ts +38 -0
- package/dist/lib/neurolink.js +1890 -1707
- package/dist/lib/providers/googleAiStudio.js +0 -5
- package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/lib/providers/googleNativeGemini3.js +39 -1
- package/dist/lib/providers/googleVertex.d.ts +10 -0
- package/dist/lib/providers/googleVertex.js +445 -445
- package/dist/lib/providers/litellm.d.ts +1 -0
- package/dist/lib/providers/litellm.js +73 -64
- package/dist/lib/providers/ollama.js +17 -4
- package/dist/lib/providers/openAI.d.ts +2 -0
- package/dist/lib/providers/openAI.js +139 -140
- package/dist/lib/proxy/claudeFormat.js +14 -5
- package/dist/lib/proxy/oauthFetch.js +298 -318
- package/dist/lib/proxy/proxyConfig.js +3 -1
- package/dist/lib/proxy/proxyFetch.js +250 -222
- package/dist/lib/proxy/proxyHealth.d.ts +17 -0
- package/dist/lib/proxy/proxyHealth.js +55 -0
- package/dist/lib/proxy/requestLogger.js +140 -48
- package/dist/lib/proxy/routingPolicy.d.ts +33 -0
- package/dist/lib/proxy/routingPolicy.js +255 -0
- package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
- package/dist/lib/proxy/snapshotPersistence.js +41 -0
- package/dist/lib/proxy/sseInterceptor.js +36 -11
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +2916 -2377
- package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
- package/dist/lib/tasks/store/redisTaskStore.js +42 -17
- package/dist/lib/tasks/taskManager.d.ts +2 -0
- package/dist/lib/tasks/taskManager.js +100 -5
- package/dist/lib/telemetry/telemetryService.js +9 -5
- package/dist/lib/types/cli.d.ts +4 -0
- package/dist/lib/types/proxyTypes.d.ts +211 -1
- package/dist/lib/types/tools.d.ts +18 -0
- package/dist/lib/utils/providerHealth.d.ts +1 -0
- package/dist/lib/utils/providerHealth.js +46 -31
- package/dist/lib/utils/providerUtils.js +11 -22
- package/dist/lib/utils/schemaConversion.d.ts +1 -0
- package/dist/lib/utils/schemaConversion.js +3 -0
- package/dist/mcp/toolRegistry.d.ts +2 -0
- package/dist/mcp/toolRegistry.js +32 -31
- package/dist/neurolink.d.ts +38 -0
- package/dist/neurolink.js +1890 -1707
- package/dist/providers/googleAiStudio.js +0 -5
- package/dist/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/providers/googleNativeGemini3.js +39 -1
- package/dist/providers/googleVertex.d.ts +10 -0
- package/dist/providers/googleVertex.js +445 -445
- package/dist/providers/litellm.d.ts +1 -0
- package/dist/providers/litellm.js +73 -64
- package/dist/providers/ollama.js +17 -4
- package/dist/providers/openAI.d.ts +2 -0
- package/dist/providers/openAI.js +139 -140
- package/dist/proxy/claudeFormat.js +14 -5
- package/dist/proxy/oauthFetch.js +298 -318
- package/dist/proxy/proxyConfig.js +3 -1
- package/dist/proxy/proxyFetch.js +250 -222
- package/dist/proxy/proxyHealth.d.ts +17 -0
- package/dist/proxy/proxyHealth.js +54 -0
- package/dist/proxy/requestLogger.js +140 -48
- package/dist/proxy/routingPolicy.d.ts +33 -0
- package/dist/proxy/routingPolicy.js +254 -0
- package/dist/proxy/snapshotPersistence.d.ts +2 -0
- package/dist/proxy/snapshotPersistence.js +40 -0
- package/dist/proxy/sseInterceptor.js +36 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/server/routes/claudeProxyRoutes.js +2916 -2377
- package/dist/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/tasks/backends/bullmqBackend.js +24 -18
- package/dist/tasks/store/redisTaskStore.js +42 -17
- package/dist/tasks/taskManager.d.ts +2 -0
- package/dist/tasks/taskManager.js +100 -5
- package/dist/telemetry/telemetryService.js +9 -5
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/proxyTypes.d.ts +211 -1
- package/dist/types/tools.d.ts +18 -0
- package/dist/utils/providerHealth.d.ts +1 -0
- package/dist/utils/providerHealth.js +46 -31
- package/dist/utils/providerUtils.js +12 -22
- package/dist/utils/schemaConversion.d.ts +1 -0
- package/dist/utils/schemaConversion.js +3 -0
- package/package.json +3 -2
- package/scripts/observability/check-proxy-telemetry.mjs +1 -1
- package/scripts/observability/manage-local-openobserve.sh +36 -5
|
@@ -29,6 +29,7 @@ export declare class LiteLLMProvider extends BaseProvider {
|
|
|
29
29
|
* Note: This is only used when tools are disabled
|
|
30
30
|
*/
|
|
31
31
|
protected executeStream(options: StreamOptions, analysisSchema?: ZodType | Schema<unknown>): Promise<StreamResult>;
|
|
32
|
+
private createLiteLLMTransformedStream;
|
|
32
33
|
/**
|
|
33
34
|
* Generate an embedding for a single text input
|
|
34
35
|
* Uses the LiteLLM proxy with OpenAI-compatible embedding API
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
2
2
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
3
|
-
import { NoOutputGeneratedError, Output, streamText } from "ai";
|
|
3
|
+
import { NoOutputGeneratedError, Output, streamText, } from "ai";
|
|
4
4
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
5
5
|
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
|
|
6
6
|
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
|
|
@@ -10,7 +10,7 @@ import { isAbortError } from "../utils/errorHandling.js";
|
|
|
10
10
|
import { logger } from "../utils/logger.js";
|
|
11
11
|
import { calculateCost } from "../utils/pricing.js";
|
|
12
12
|
import { getProviderModel } from "../utils/providerConfig.js";
|
|
13
|
-
import { composeAbortSignals, createTimeoutController, TimeoutError } from "../utils/timeout.js";
|
|
13
|
+
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
14
14
|
import { resolveToolChoice } from "../utils/toolChoice.js";
|
|
15
15
|
import { getModelId } from "./providerTypeUtils.js";
|
|
16
16
|
const streamTracer = trace.getTracer("neurolink.provider.litellm");
|
|
@@ -86,15 +86,18 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
86
86
|
// Check for timeout by error name and message as fallback
|
|
87
87
|
const errorRecord = error;
|
|
88
88
|
if (errorRecord?.name === "TimeoutError" ||
|
|
89
|
-
(typeof errorRecord?.message === "string" &&
|
|
89
|
+
(typeof errorRecord?.message === "string" &&
|
|
90
|
+
errorRecord.message.toLowerCase().includes("timeout"))) {
|
|
90
91
|
return new NetworkError(`Request timed out: ${errorRecord?.message || "Unknown timeout"}`, this.providerName);
|
|
91
92
|
}
|
|
92
93
|
if (typeof errorRecord?.message === "string") {
|
|
93
|
-
if (errorRecord.message.includes("ECONNREFUSED") ||
|
|
94
|
+
if (errorRecord.message.includes("ECONNREFUSED") ||
|
|
95
|
+
errorRecord.message.includes("Failed to fetch")) {
|
|
94
96
|
return new NetworkError("LiteLLM proxy server not available. Please start the LiteLLM proxy server at " +
|
|
95
97
|
`${process.env.LITELLM_BASE_URL || "http://localhost:4000"}`, this.providerName);
|
|
96
98
|
}
|
|
97
|
-
if (errorRecord.message.includes("API_KEY_INVALID") ||
|
|
99
|
+
if (errorRecord.message.includes("API_KEY_INVALID") ||
|
|
100
|
+
errorRecord.message.includes("Invalid API key")) {
|
|
98
101
|
return new AuthenticationError("Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.", this.providerName);
|
|
99
102
|
}
|
|
100
103
|
if (errorRecord.message.toLowerCase().includes("rate limit")) {
|
|
@@ -131,7 +134,9 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
131
134
|
const model = await this.getAISDKModelWithMiddleware(options); // This is where network connection happens!
|
|
132
135
|
// Get tools - options.tools is pre-merged by BaseProvider.stream()
|
|
133
136
|
const shouldUseTools = !options.disableTools && this.supportsTools();
|
|
134
|
-
const tools = shouldUseTools
|
|
137
|
+
const tools = shouldUseTools
|
|
138
|
+
? options.tools || (await this.getAllTools())
|
|
139
|
+
: {};
|
|
135
140
|
logger.debug(`LiteLLM: Tools for streaming`, {
|
|
136
141
|
shouldUseTools,
|
|
137
142
|
toolCount: Object.keys(tools).length,
|
|
@@ -188,7 +193,8 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
188
193
|
toolName: toolCall.toolName,
|
|
189
194
|
args: toolCall.args ??
|
|
190
195
|
toolCall.input ??
|
|
191
|
-
toolCall
|
|
196
|
+
toolCall
|
|
197
|
+
.parameters ??
|
|
192
198
|
{},
|
|
193
199
|
});
|
|
194
200
|
}
|
|
@@ -197,7 +203,8 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
197
203
|
collectedToolResults.push({
|
|
198
204
|
toolName: toolResult.toolName,
|
|
199
205
|
status: rawToolResult.error ? "failure" : "success",
|
|
200
|
-
output: (rawToolResult.output ??
|
|
206
|
+
output: (rawToolResult.output ??
|
|
207
|
+
rawToolResult.result) ?? undefined,
|
|
201
208
|
error: rawToolResult.error,
|
|
202
209
|
id: rawToolResult.toolCallId ?? toolResult.toolName,
|
|
203
210
|
});
|
|
@@ -243,7 +250,9 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
243
250
|
catch (streamError) {
|
|
244
251
|
streamSpan.setStatus({
|
|
245
252
|
code: SpanStatusCode.ERROR,
|
|
246
|
-
message: streamError instanceof Error
|
|
253
|
+
message: streamError instanceof Error
|
|
254
|
+
? streamError.message
|
|
255
|
+
: String(streamError),
|
|
247
256
|
});
|
|
248
257
|
streamSpan.end();
|
|
249
258
|
throw streamError;
|
|
@@ -285,59 +294,11 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
285
294
|
streamSpan.end();
|
|
286
295
|
});
|
|
287
296
|
timeoutController?.cleanup();
|
|
288
|
-
|
|
289
|
-
// Note: fullStream includes tool results, textStream only has text
|
|
290
|
-
const transformedStream = (async function* () {
|
|
291
|
-
try {
|
|
292
|
-
// Try fullStream first (handles both text and tool calls), fallback to textStream
|
|
293
|
-
const streamToUse = result.fullStream || result.textStream;
|
|
294
|
-
for await (const chunk of streamToUse) {
|
|
295
|
-
// Handle different chunk types from fullStream
|
|
296
|
-
if (chunk && typeof chunk === "object") {
|
|
297
|
-
// Check for error chunks first (critical error handling)
|
|
298
|
-
if ("type" in chunk && chunk.type === "error") {
|
|
299
|
-
const errorChunk = chunk;
|
|
300
|
-
logger.error(`LiteLLM: Error chunk received:`, {
|
|
301
|
-
errorType: errorChunk.type,
|
|
302
|
-
errorDetails: errorChunk.error,
|
|
303
|
-
});
|
|
304
|
-
throw new Error(`LiteLLM streaming error: ${errorChunk.error?.message || "Unknown error"}`);
|
|
305
|
-
}
|
|
306
|
-
if ("textDelta" in chunk) {
|
|
307
|
-
// Text delta from fullStream
|
|
308
|
-
const textDelta = chunk.textDelta;
|
|
309
|
-
if (textDelta) {
|
|
310
|
-
yield { content: textDelta };
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
else if ("type" in chunk && chunk.type === "tool-call" && "toolCallId" in chunk) {
|
|
314
|
-
// Tool call event - log for debugging
|
|
315
|
-
const toolCallId = String(chunk.toolCallId);
|
|
316
|
-
const toolName = "toolName" in chunk ? String(chunk.toolName) : "unknown";
|
|
317
|
-
logger.debug("LiteLLM: Tool call", {
|
|
318
|
-
toolCallId,
|
|
319
|
-
toolName,
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
else if (typeof chunk === "string") {
|
|
324
|
-
// Direct string chunk from textStream fallback
|
|
325
|
-
yield { content: chunk };
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
catch (streamError) {
|
|
330
|
-
// AI SDK v6 throws NoOutputGeneratedError when the stream produced no output.
|
|
331
|
-
if (NoOutputGeneratedError.isInstance(streamError)) {
|
|
332
|
-
logger.warn("LiteLLM: Stream produced no output (NoOutputGeneratedError)");
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
throw streamError;
|
|
336
|
-
}
|
|
337
|
-
})();
|
|
297
|
+
const transformedStream = this.createLiteLLMTransformedStream(result);
|
|
338
298
|
// Create analytics promise that resolves after stream completion
|
|
339
299
|
const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
|
|
340
|
-
requestId: options.requestId ??
|
|
300
|
+
requestId: options.requestId ??
|
|
301
|
+
`litellm-stream-${Date.now()}`,
|
|
341
302
|
streamingMode: true,
|
|
342
303
|
});
|
|
343
304
|
return {
|
|
@@ -360,6 +321,47 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
360
321
|
throw this.handleProviderError(error);
|
|
361
322
|
}
|
|
362
323
|
}
|
|
324
|
+
async *createLiteLLMTransformedStream(result) {
|
|
325
|
+
try {
|
|
326
|
+
const streamToUse = result.fullStream || result.textStream;
|
|
327
|
+
for await (const chunk of streamToUse) {
|
|
328
|
+
if (chunk && typeof chunk === "object") {
|
|
329
|
+
if ("type" in chunk && chunk.type === "error") {
|
|
330
|
+
const errorChunk = chunk;
|
|
331
|
+
logger.error(`LiteLLM: Error chunk received:`, {
|
|
332
|
+
errorType: errorChunk.type,
|
|
333
|
+
errorDetails: errorChunk.error,
|
|
334
|
+
});
|
|
335
|
+
throw this.formatProviderError(new Error(`LiteLLM streaming error: ${errorChunk.error?.message || "Unknown error"}`));
|
|
336
|
+
}
|
|
337
|
+
if ("textDelta" in chunk) {
|
|
338
|
+
const textDelta = chunk.textDelta;
|
|
339
|
+
if (textDelta) {
|
|
340
|
+
yield { content: textDelta };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else if ("type" in chunk &&
|
|
344
|
+
chunk.type === "tool-call" &&
|
|
345
|
+
"toolCallId" in chunk) {
|
|
346
|
+
logger.debug("LiteLLM: Tool call", {
|
|
347
|
+
toolCallId: String(chunk.toolCallId),
|
|
348
|
+
toolName: "toolName" in chunk ? String(chunk.toolName) : "unknown",
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (typeof chunk === "string") {
|
|
353
|
+
yield { content: chunk };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch (streamError) {
|
|
358
|
+
if (NoOutputGeneratedError.isInstance(streamError)) {
|
|
359
|
+
logger.warn("LiteLLM: Stream produced no output (NoOutputGeneratedError)");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
throw streamError;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
363
365
|
/**
|
|
364
366
|
* Generate an embedding for a single text input
|
|
365
367
|
* Uses the LiteLLM proxy with OpenAI-compatible embedding API
|
|
@@ -368,7 +370,9 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
368
370
|
const { embed: aiEmbed } = await import("ai");
|
|
369
371
|
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
370
372
|
const config = getLiteLLMConfig();
|
|
371
|
-
const embeddingModelName = modelName ||
|
|
373
|
+
const embeddingModelName = modelName ||
|
|
374
|
+
process.env.LITELLM_EMBEDDING_MODEL ||
|
|
375
|
+
"gemini-embedding-001";
|
|
372
376
|
const customOpenAI = createOpenAI({
|
|
373
377
|
baseURL: config.baseURL,
|
|
374
378
|
apiKey: config.apiKey,
|
|
@@ -386,7 +390,9 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
386
390
|
const { embedMany: aiEmbedMany } = await import("ai");
|
|
387
391
|
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
388
392
|
const config = getLiteLLMConfig();
|
|
389
|
-
const embeddingModelName = modelName ||
|
|
393
|
+
const embeddingModelName = modelName ||
|
|
394
|
+
process.env.LITELLM_EMBEDDING_MODEL ||
|
|
395
|
+
"gemini-embedding-001";
|
|
390
396
|
const customOpenAI = createOpenAI({
|
|
391
397
|
baseURL: config.baseURL,
|
|
392
398
|
apiKey: config.apiKey,
|
|
@@ -405,7 +411,8 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
405
411
|
const now = Date.now();
|
|
406
412
|
// Check if cached models are still valid
|
|
407
413
|
if (LiteLLMProvider.modelsCache.length > 0 &&
|
|
408
|
-
now - LiteLLMProvider.modelsCacheTime <
|
|
414
|
+
now - LiteLLMProvider.modelsCacheTime <
|
|
415
|
+
LiteLLMProvider.MODELS_CACHE_DURATION) {
|
|
409
416
|
logger.debug(`[${functionTag}] Using cached models`, {
|
|
410
417
|
cacheAge: Math.round((now - LiteLLMProvider.modelsCacheTime) / 1000),
|
|
411
418
|
modelCount: LiteLLMProvider.modelsCache.length,
|
|
@@ -431,7 +438,9 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
431
438
|
});
|
|
432
439
|
}
|
|
433
440
|
// Fallback to hardcoded list if API fetch fails
|
|
434
|
-
const fallbackModels = process.env.LITELLM_FALLBACK_MODELS?.split(",")
|
|
441
|
+
const fallbackModels = process.env.LITELLM_FALLBACK_MODELS?.split(",")
|
|
442
|
+
.map((m) => m.trim())
|
|
443
|
+
.filter((m) => m.length > 0) || [
|
|
435
444
|
"openai/gpt-4o", // minimal safe baseline
|
|
436
445
|
"anthropic/claude-3-haiku",
|
|
437
446
|
"meta-llama/llama-3.1-8b-instruct",
|
package/dist/providers/ollama.js
CHANGED
|
@@ -40,6 +40,11 @@ const getOllamaTimeout = () => {
|
|
|
40
40
|
// especially for larger models like aliafshar/gemma3-it-qat-tools:latest (12.2B parameters)
|
|
41
41
|
return parseInt(process.env.OLLAMA_TIMEOUT || "240000", 10);
|
|
42
42
|
};
|
|
43
|
+
function isOllamaHttpError(error) {
|
|
44
|
+
return (error instanceof ProviderError &&
|
|
45
|
+
typeof error.statusCode === "number" &&
|
|
46
|
+
typeof error.responseBody === "string");
|
|
47
|
+
}
|
|
43
48
|
async function createOllamaHttpError(response) {
|
|
44
49
|
let responseBody = "";
|
|
45
50
|
try {
|
|
@@ -49,7 +54,11 @@ async function createOllamaHttpError(response) {
|
|
|
49
54
|
// Ignore unreadable bodies
|
|
50
55
|
}
|
|
51
56
|
const suffix = responseBody ? ` - ${responseBody.slice(0, 500)}` : "";
|
|
52
|
-
|
|
57
|
+
const error = new ProviderError(`Ollama API error: ${response.status} ${response.statusText}${suffix}`, "ollama");
|
|
58
|
+
error.statusCode = response.status;
|
|
59
|
+
error.statusText = response.statusText;
|
|
60
|
+
error.responseBody = responseBody;
|
|
61
|
+
return error;
|
|
53
62
|
}
|
|
54
63
|
// Create proxy-aware fetch instance
|
|
55
64
|
const proxyFetch = createProxyFetch();
|
|
@@ -1538,12 +1547,16 @@ export class OllamaProvider extends BaseProvider {
|
|
|
1538
1547
|
return new InvalidModelError(`❌ Ollama Model Not Found\n\nModel '${this.modelName}' is not available locally.\n\n🔧 Install Model:\n1. Run: ollama pull ${this.modelName}\n2. Or try a different model:\n - ollama pull ${FALLBACK_OLLAMA_MODEL}\n - ollama pull mistral:latest\n - ollama pull codellama:latest\n\n🔧 List Available Models:\nollama list`, this.providerName);
|
|
1539
1548
|
}
|
|
1540
1549
|
const errMsg = error.message ?? "";
|
|
1541
|
-
|
|
1542
|
-
|
|
1550
|
+
const httpStatus = isOllamaHttpError(error) ? error.statusCode : undefined;
|
|
1551
|
+
const responseBody = isOllamaHttpError(error) ? error.responseBody : "";
|
|
1552
|
+
if (httpStatus === 404 &&
|
|
1553
|
+
(responseBody.toLowerCase().includes("model") ||
|
|
1554
|
+
responseBody.toLowerCase().includes("not found") ||
|
|
1555
|
+
errMsg.toLowerCase().includes("model") ||
|
|
1543
1556
|
errMsg.toLowerCase().includes("not found"))) {
|
|
1544
1557
|
return new InvalidModelError(`❌ Ollama Returned HTTP 404\n\nThis usually means the configured model '${this.modelName}' is not installed locally, although a bad base URL or incompatible API mode can also cause it.\n\n🔧 Check:\n1. Verify the model exists: 'ollama list'\n2. Pull it if missing: 'ollama pull ${this.modelName}'\n3. Verify the service is healthy: 'curl ${this.baseUrl}/api/version'\n4. If you use OpenAI-compatible mode, confirm the base URL serves /v1/chat/completions`, this.providerName);
|
|
1545
1558
|
}
|
|
1546
|
-
if (
|
|
1559
|
+
if (httpStatus === 404) {
|
|
1547
1560
|
return new ProviderError(`❌ Ollama Endpoint Returned HTTP 404\n\nThe configured base URL (${this.baseUrl}) did not serve the expected Ollama endpoint for model '${this.modelName}'. This is usually a configuration or API-mode mismatch rather than a missing model.\n\n🔧 Check:\n1. Verify the base URL: ${this.baseUrl}\n2. For native Ollama mode, confirm /api/generate exists\n3. For OpenAI-compatible mode, confirm /v1/chat/completions exists\n4. If the model is missing, the response body should explicitly say so`, this.providerName);
|
|
1548
1561
|
}
|
|
1549
1562
|
return new ProviderError(`❌ Ollama Provider Error\n\n${error.message || "Unknown error occurred"}\n\n🔧 Troubleshooting:\n1. Check if Ollama service is running\n2. Verify model is installed: 'ollama list'\n3. Check network connectivity to ${this.baseUrl}\n4. Review Ollama logs for details`, this.providerName);
|
|
@@ -52,6 +52,8 @@ export declare class OpenAIProvider extends BaseProvider {
|
|
|
52
52
|
* and the migration guide in the project repository.
|
|
53
53
|
*/
|
|
54
54
|
protected executeStream(options: StreamOptions, _analysisSchema?: ValidationSchema): Promise<StreamResult>;
|
|
55
|
+
private createOpenAITransformedStream;
|
|
56
|
+
private extractOpenAIChunkContent;
|
|
55
57
|
/**
|
|
56
58
|
* Generate embeddings for text using OpenAI text-embedding models
|
|
57
59
|
* @param text - The text to embed
|
package/dist/providers/openAI.js
CHANGED
|
@@ -279,6 +279,16 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
279
279
|
// Build message array from options with multimodal support
|
|
280
280
|
// Using protected helper from BaseProvider to eliminate code duplication
|
|
281
281
|
const messages = await this.buildMessagesForStream(options);
|
|
282
|
+
let resolvedToolChoice = resolveToolChoice(options, tools, shouldUseTools);
|
|
283
|
+
// Guard: if toolChoice names a specific tool that was filtered out, fall back to "auto"
|
|
284
|
+
if (resolvedToolChoice !== null &&
|
|
285
|
+
typeof resolvedToolChoice === "object" &&
|
|
286
|
+
"toolName" in resolvedToolChoice &&
|
|
287
|
+
typeof resolvedToolChoice.toolName === "string" &&
|
|
288
|
+
!tools[resolvedToolChoice.toolName]) {
|
|
289
|
+
logger.warn(`OpenAI: toolChoice references tool "${resolvedToolChoice.toolName}" which was removed during filtering; falling back to "auto"`);
|
|
290
|
+
resolvedToolChoice = "auto";
|
|
291
|
+
}
|
|
282
292
|
// Debug the actual request being sent to OpenAI
|
|
283
293
|
logger.debug(`OpenAI: streamText request parameters:`, {
|
|
284
294
|
modelName: this.modelName,
|
|
@@ -286,7 +296,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
286
296
|
temperature: options.temperature,
|
|
287
297
|
maxTokens: options.maxTokens,
|
|
288
298
|
toolsCount: Object.keys(tools).length,
|
|
289
|
-
toolChoice:
|
|
299
|
+
toolChoice: resolvedToolChoice,
|
|
290
300
|
maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
|
|
291
301
|
firstToolExample: Object.keys(tools).length > 0
|
|
292
302
|
? {
|
|
@@ -315,7 +325,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
315
325
|
maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
|
|
316
326
|
tools,
|
|
317
327
|
stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
|
|
318
|
-
toolChoice:
|
|
328
|
+
toolChoice: resolvedToolChoice,
|
|
319
329
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
320
330
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
321
331
|
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
@@ -382,150 +392,14 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
382
392
|
hasToolResults: !!result.toolResults,
|
|
383
393
|
resultType: typeof result,
|
|
384
394
|
});
|
|
385
|
-
|
|
386
|
-
const transformedStream = async function* () {
|
|
387
|
-
try {
|
|
388
|
-
logger.debug(`OpenAI: Starting stream transformation`, {
|
|
389
|
-
hasTextStream: !!result.textStream,
|
|
390
|
-
hasFullStream: !!result.fullStream,
|
|
391
|
-
resultKeys: Object.keys(result),
|
|
392
|
-
toolsEnabled: shouldUseTools,
|
|
393
|
-
toolsCount: Object.keys(tools).length,
|
|
394
|
-
});
|
|
395
|
-
let chunkCount = 0;
|
|
396
|
-
let contentYielded = 0;
|
|
397
|
-
// Try fullStream first (handles both text and tool calls), fallback to textStream
|
|
398
|
-
const streamToUse = result.fullStream || result.textStream;
|
|
399
|
-
if (!streamToUse) {
|
|
400
|
-
logger.error("OpenAI: No stream available in result", {
|
|
401
|
-
resultKeys: Object.keys(result),
|
|
402
|
-
});
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
logger.debug(`OpenAI: Stream source selected:`, {
|
|
406
|
-
usingFullStream: !!result.fullStream,
|
|
407
|
-
usingTextStream: !!result.textStream && !result.fullStream,
|
|
408
|
-
streamSourceType: result.fullStream ? "fullStream" : "textStream",
|
|
409
|
-
});
|
|
410
|
-
for await (const chunk of streamToUse) {
|
|
411
|
-
chunkCount++;
|
|
412
|
-
logger.debug(`OpenAI: Processing chunk ${chunkCount}:`, {
|
|
413
|
-
chunkType: typeof chunk,
|
|
414
|
-
chunkValue: typeof chunk === "string"
|
|
415
|
-
? chunk.substring(0, 50)
|
|
416
|
-
: "not-string",
|
|
417
|
-
chunkKeys: chunk && typeof chunk === "object"
|
|
418
|
-
? Object.keys(chunk)
|
|
419
|
-
: "not-object",
|
|
420
|
-
hasText: chunk && typeof chunk === "object" && "text" in chunk,
|
|
421
|
-
hasTextDelta: chunk && typeof chunk === "object" && "textDelta" in chunk,
|
|
422
|
-
hasType: chunk && typeof chunk === "object" && "type" in chunk,
|
|
423
|
-
chunkTypeValue: chunk && typeof chunk === "object" && "type" in chunk
|
|
424
|
-
? chunk.type
|
|
425
|
-
: "no-type",
|
|
426
|
-
});
|
|
427
|
-
let contentToYield = null;
|
|
428
|
-
// Handle different chunk types from fullStream
|
|
429
|
-
if (chunk && typeof chunk === "object") {
|
|
430
|
-
// Log the full chunk structure for debugging (debug mode only)
|
|
431
|
-
if (process.env.NEUROLINK_DEBUG === "true") {
|
|
432
|
-
logger.debug(`OpenAI: Full chunk structure:`, {
|
|
433
|
-
chunkKeys: Object.keys(chunk),
|
|
434
|
-
fullChunk: JSON.stringify(chunk).substring(0, 500),
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
if ("type" in chunk && chunk.type === "error") {
|
|
438
|
-
// Handle error chunks when tools are enabled
|
|
439
|
-
const errorChunk = chunk;
|
|
440
|
-
logger.error(`OpenAI: Error chunk received:`, {
|
|
441
|
-
errorType: errorChunk.type,
|
|
442
|
-
errorDetails: errorChunk.error,
|
|
443
|
-
fullChunk: JSON.stringify(chunk),
|
|
444
|
-
});
|
|
445
|
-
// Throw a more descriptive error for tool-related issues
|
|
446
|
-
const errorMessage = errorChunk.error &&
|
|
447
|
-
typeof errorChunk.error === "object" &&
|
|
448
|
-
"message" in errorChunk.error
|
|
449
|
-
? String(errorChunk.error.message)
|
|
450
|
-
: "OpenAI API error when tools are enabled";
|
|
451
|
-
throw new Error(`OpenAI streaming error with tools: ${errorMessage}. Try disabling tools with --disableTools`);
|
|
452
|
-
}
|
|
453
|
-
else if ("type" in chunk &&
|
|
454
|
-
chunk.type === "text-delta" &&
|
|
455
|
-
"textDelta" in chunk) {
|
|
456
|
-
// Text delta from fullStream
|
|
457
|
-
contentToYield = chunk.textDelta;
|
|
458
|
-
logger.debug(`OpenAI: Found text-delta:`, {
|
|
459
|
-
textDelta: contentToYield,
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
else if ("text" in chunk) {
|
|
463
|
-
// Direct text chunk
|
|
464
|
-
contentToYield = chunk.text;
|
|
465
|
-
logger.debug(`OpenAI: Found direct text:`, {
|
|
466
|
-
text: contentToYield,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
// Log unhandled chunks in debug mode only
|
|
471
|
-
if (process.env.NEUROLINK_DEBUG === "true") {
|
|
472
|
-
logger.debug(`OpenAI: Unhandled object chunk:`, {
|
|
473
|
-
chunkKeys: Object.keys(chunk),
|
|
474
|
-
chunkType: chunk.type || "no-type",
|
|
475
|
-
fullChunk: JSON.stringify(chunk).substring(0, 500),
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
else if (typeof chunk === "string") {
|
|
481
|
-
// Direct string chunk from textStream
|
|
482
|
-
contentToYield = chunk;
|
|
483
|
-
logger.debug(`OpenAI: Found string chunk:`, {
|
|
484
|
-
content: contentToYield,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
logger.warn(`OpenAI: Unhandled chunk type:`, {
|
|
489
|
-
type: typeof chunk,
|
|
490
|
-
value: String(chunk).substring(0, 100),
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
if (contentToYield) {
|
|
494
|
-
contentYielded++;
|
|
495
|
-
logger.debug(`OpenAI: Yielding content ${contentYielded}:`, {
|
|
496
|
-
content: contentToYield.substring(0, 50),
|
|
497
|
-
length: contentToYield.length,
|
|
498
|
-
});
|
|
499
|
-
yield { content: contentToYield };
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
logger.debug(`OpenAI: Stream transformation completed`, {
|
|
503
|
-
totalChunks: chunkCount,
|
|
504
|
-
contentYielded,
|
|
505
|
-
success: contentYielded > 0,
|
|
506
|
-
});
|
|
507
|
-
if (contentYielded === 0) {
|
|
508
|
-
logger.warn(`OpenAI: No content was yielded from stream despite processing ${chunkCount} chunks`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
catch (streamError) {
|
|
512
|
-
// AI SDK v6 throws NoOutputGeneratedError when the stream produced no output.
|
|
513
|
-
// Treat as an empty stream rather than crashing with an unhandled rejection.
|
|
514
|
-
if (NoOutputGeneratedError.isInstance(streamError)) {
|
|
515
|
-
logger.warn("OpenAI: Stream produced no output (NoOutputGeneratedError)");
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
logger.error(`OpenAI: Stream transformation error:`, streamError);
|
|
519
|
-
throw streamError;
|
|
520
|
-
}
|
|
521
|
-
};
|
|
395
|
+
const transformedStream = this.createOpenAITransformedStream(result, shouldUseTools, tools);
|
|
522
396
|
// Create analytics promise that resolves after stream completion
|
|
523
397
|
const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
|
|
524
398
|
requestId: `openai-stream-${Date.now()}`,
|
|
525
399
|
streamingMode: true,
|
|
526
400
|
});
|
|
527
401
|
return {
|
|
528
|
-
stream: transformedStream
|
|
402
|
+
stream: transformedStream,
|
|
529
403
|
provider: this.providerName,
|
|
530
404
|
model: this.modelName,
|
|
531
405
|
analytics: analyticsPromise,
|
|
@@ -540,6 +414,131 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
540
414
|
throw this.handleProviderError(error);
|
|
541
415
|
}
|
|
542
416
|
}
|
|
417
|
+
async *createOpenAITransformedStream(result, shouldUseTools, tools) {
|
|
418
|
+
try {
|
|
419
|
+
logger.debug(`OpenAI: Starting stream transformation`, {
|
|
420
|
+
hasTextStream: !!result.textStream,
|
|
421
|
+
hasFullStream: !!result.fullStream,
|
|
422
|
+
resultKeys: Object.keys(result),
|
|
423
|
+
toolsEnabled: shouldUseTools,
|
|
424
|
+
toolsCount: Object.keys(tools).length,
|
|
425
|
+
});
|
|
426
|
+
let chunkCount = 0;
|
|
427
|
+
let contentYielded = 0;
|
|
428
|
+
const streamToUse = result.fullStream || result.textStream;
|
|
429
|
+
if (!streamToUse) {
|
|
430
|
+
logger.error("OpenAI: No stream available in result", {
|
|
431
|
+
resultKeys: Object.keys(result),
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
logger.debug(`OpenAI: Stream source selected:`, {
|
|
436
|
+
usingFullStream: !!result.fullStream,
|
|
437
|
+
usingTextStream: !!result.textStream && !result.fullStream,
|
|
438
|
+
streamSourceType: result.fullStream ? "fullStream" : "textStream",
|
|
439
|
+
});
|
|
440
|
+
for await (const chunk of streamToUse) {
|
|
441
|
+
chunkCount++;
|
|
442
|
+
logger.debug(`OpenAI: Processing chunk ${chunkCount}:`, {
|
|
443
|
+
chunkType: typeof chunk,
|
|
444
|
+
chunkValue: typeof chunk === "string"
|
|
445
|
+
? chunk.substring(0, 50)
|
|
446
|
+
: "not-string",
|
|
447
|
+
chunkKeys: chunk && typeof chunk === "object"
|
|
448
|
+
? Object.keys(chunk)
|
|
449
|
+
: "not-object",
|
|
450
|
+
hasText: chunk && typeof chunk === "object" && "text" in chunk,
|
|
451
|
+
hasTextDelta: chunk && typeof chunk === "object" && "textDelta" in chunk,
|
|
452
|
+
hasType: chunk && typeof chunk === "object" && "type" in chunk,
|
|
453
|
+
chunkTypeValue: chunk && typeof chunk === "object" && "type" in chunk
|
|
454
|
+
? chunk.type
|
|
455
|
+
: "no-type",
|
|
456
|
+
});
|
|
457
|
+
const contentToYield = this.extractOpenAIChunkContent(chunk);
|
|
458
|
+
if (contentToYield) {
|
|
459
|
+
contentYielded++;
|
|
460
|
+
logger.debug(`OpenAI: Yielding content ${contentYielded}:`, {
|
|
461
|
+
content: contentToYield.substring(0, 50),
|
|
462
|
+
length: contentToYield.length,
|
|
463
|
+
});
|
|
464
|
+
yield { content: contentToYield };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
logger.debug(`OpenAI: Stream transformation completed`, {
|
|
468
|
+
totalChunks: chunkCount,
|
|
469
|
+
contentYielded,
|
|
470
|
+
success: contentYielded > 0,
|
|
471
|
+
});
|
|
472
|
+
if (contentYielded === 0) {
|
|
473
|
+
logger.warn(`OpenAI: No content was yielded from stream despite processing ${chunkCount} chunks`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (streamError) {
|
|
477
|
+
if (NoOutputGeneratedError.isInstance(streamError)) {
|
|
478
|
+
logger.warn("OpenAI: Stream produced no output (NoOutputGeneratedError)");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
logger.error(`OpenAI: Stream transformation error:`, streamError);
|
|
482
|
+
throw streamError;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
extractOpenAIChunkContent(chunk) {
|
|
486
|
+
if (chunk && typeof chunk === "object") {
|
|
487
|
+
if (process.env.NEUROLINK_DEBUG === "true") {
|
|
488
|
+
logger.debug(`OpenAI: Full chunk structure:`, {
|
|
489
|
+
chunkKeys: Object.keys(chunk),
|
|
490
|
+
fullChunk: JSON.stringify(chunk).substring(0, 500),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
if ("type" in chunk && chunk.type === "error") {
|
|
494
|
+
const errorChunk = chunk;
|
|
495
|
+
logger.error(`OpenAI: Error chunk received:`, {
|
|
496
|
+
errorType: errorChunk.type,
|
|
497
|
+
errorDetails: errorChunk.error,
|
|
498
|
+
fullChunk: JSON.stringify(chunk),
|
|
499
|
+
});
|
|
500
|
+
const errorMessage = errorChunk.error &&
|
|
501
|
+
typeof errorChunk.error === "object" &&
|
|
502
|
+
"message" in errorChunk.error
|
|
503
|
+
? String(errorChunk.error.message)
|
|
504
|
+
: "OpenAI API error when tools are enabled";
|
|
505
|
+
throw new Error(`OpenAI streaming error with tools: ${errorMessage}. Try disabling tools with --disableTools`);
|
|
506
|
+
}
|
|
507
|
+
if ("type" in chunk &&
|
|
508
|
+
chunk.type === "text-delta" &&
|
|
509
|
+
"textDelta" in chunk) {
|
|
510
|
+
const textDelta = chunk.textDelta;
|
|
511
|
+
logger.debug(`OpenAI: Found text-delta:`, { textDelta });
|
|
512
|
+
return textDelta;
|
|
513
|
+
}
|
|
514
|
+
if ("text" in chunk) {
|
|
515
|
+
const text = chunk.text;
|
|
516
|
+
logger.debug(`OpenAI: Found direct text:`, { text });
|
|
517
|
+
return text;
|
|
518
|
+
}
|
|
519
|
+
if (process.env.NEUROLINK_DEBUG === "true") {
|
|
520
|
+
logger.debug(`OpenAI: Unhandled object chunk:`, {
|
|
521
|
+
chunkKeys: Object.keys(chunk),
|
|
522
|
+
chunkType: "type" in chunk
|
|
523
|
+
? String(chunk.type)
|
|
524
|
+
: "no-type",
|
|
525
|
+
fullChunk: JSON.stringify(chunk).substring(0, 500),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
if (typeof chunk === "string") {
|
|
531
|
+
logger.debug(`OpenAI: Found string chunk:`, {
|
|
532
|
+
content: chunk,
|
|
533
|
+
});
|
|
534
|
+
return chunk;
|
|
535
|
+
}
|
|
536
|
+
logger.warn(`OpenAI: Unhandled chunk type:`, {
|
|
537
|
+
type: typeof chunk,
|
|
538
|
+
value: String(chunk).substring(0, 100),
|
|
539
|
+
});
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
543
542
|
/**
|
|
544
543
|
* Generate embeddings for text using OpenAI text-embedding models
|
|
545
544
|
* @param text - The text to embed
|