@juspay/neurolink 11.0.0 → 11.1.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 +6 -0
- package/dist/browser/neurolink.min.js +524 -524
- package/dist/cli/commands/setup.d.ts +4 -0
- package/dist/cli/commands/setup.js +19 -33
- package/dist/cli/factories/commandFactory.d.ts +1157 -1
- package/dist/cli/factories/commandFactory.js +20 -53
- package/dist/factories/providerDescriptors.d.ts +18 -0
- package/dist/factories/providerDescriptors.js +536 -0
- package/dist/factories/providerFactory.d.ts +25 -14
- package/dist/factories/providerFactory.js +46 -23
- package/dist/factories/providerRegistry.js +31 -30
- package/dist/index.d.ts +10 -1
- package/dist/index.js +13 -3
- package/dist/lib/factories/providerDescriptors.d.ts +18 -0
- package/dist/lib/factories/providerDescriptors.js +537 -0
- package/dist/lib/factories/providerFactory.d.ts +25 -14
- package/dist/lib/factories/providerFactory.js +46 -23
- package/dist/lib/factories/providerRegistry.js +31 -30
- package/dist/lib/index.d.ts +10 -1
- package/dist/lib/index.js +13 -3
- package/dist/lib/neurolink.js +25 -37
- package/dist/lib/providers/amazonBedrock/client.js +34 -16
- package/dist/lib/providers/anthropic/client.js +39 -34
- package/dist/lib/providers/azureOpenai.js +17 -15
- package/dist/lib/providers/cloudflare.js +12 -21
- package/dist/lib/providers/cohere.js +31 -25
- package/dist/lib/providers/deepseek.js +23 -26
- package/dist/lib/providers/fireworks.js +12 -21
- package/dist/lib/providers/googleAiStudio/client.d.ts +2 -0
- package/dist/lib/providers/googleAiStudio/client.js +39 -17
- package/dist/lib/providers/googleVertex/client.js +107 -99
- package/dist/lib/providers/groq.js +19 -22
- package/dist/lib/providers/huggingFace/client.js +26 -24
- package/dist/lib/providers/litellm/client.js +54 -39
- package/dist/lib/providers/llamaCpp.js +21 -18
- package/dist/lib/providers/lmStudio.js +22 -19
- package/dist/lib/providers/mistral.js +12 -24
- package/dist/lib/providers/nvidiaNim/client.js +37 -34
- package/dist/lib/providers/ollama/client.js +50 -35
- package/dist/lib/providers/openAI/client.js +37 -40
- package/dist/lib/providers/openRouter/client.js +53 -47
- package/dist/lib/providers/openaiChatCompletionsBase.js +40 -16
- package/dist/lib/providers/openaiCompatible/client.js +36 -32
- package/dist/lib/providers/perplexity.js +12 -21
- package/dist/lib/providers/togetherAi.js +12 -21
- package/dist/lib/providers/xai.js +17 -26
- package/dist/lib/server/errors.d.ts +1 -1
- package/dist/lib/server/errors.js +2 -2
- package/dist/lib/server/index.d.ts +2 -2
- package/dist/lib/server/index.js +5 -3
- package/dist/lib/server/middleware/rateLimit.d.ts +0 -4
- package/dist/lib/server/middleware/rateLimit.js +0 -4
- package/dist/lib/types/errors.d.ts +35 -0
- package/dist/lib/types/providers.d.ts +64 -0
- package/dist/lib/utils/errorClassifier.d.ts +30 -0
- package/dist/lib/utils/errorClassifier.js +94 -0
- package/dist/lib/utils/fileDetector.js +6 -43
- package/dist/lib/utils/providerHealth.d.ts +42 -7
- package/dist/lib/utils/providerHealth.js +115 -122
- package/dist/lib/utils/providerUtils.js +21 -63
- package/dist/neurolink.js +25 -37
- package/dist/providers/amazonBedrock/client.js +34 -16
- package/dist/providers/amazonSagemaker.d.ts +1 -1
- package/dist/providers/anthropic/client.js +39 -34
- package/dist/providers/azureOpenai.js +17 -15
- package/dist/providers/cloudflare.js +12 -21
- package/dist/providers/cohere.js +31 -25
- package/dist/providers/deepseek.js +23 -26
- package/dist/providers/fireworks.js +12 -21
- package/dist/providers/googleAiStudio/client.d.ts +2 -0
- package/dist/providers/googleAiStudio/client.js +39 -17
- package/dist/providers/googleVertex/client.js +107 -99
- package/dist/providers/groq.js +19 -22
- package/dist/providers/huggingFace/client.js +26 -24
- package/dist/providers/litellm/client.js +54 -39
- package/dist/providers/llamaCpp.js +21 -18
- package/dist/providers/lmStudio.js +22 -19
- package/dist/providers/mistral.js +12 -24
- package/dist/providers/nvidiaNim/client.js +37 -34
- package/dist/providers/ollama/client.js +50 -35
- package/dist/providers/openAI/client.js +37 -40
- package/dist/providers/openRouter/client.js +53 -47
- package/dist/providers/openaiChatCompletionsBase.js +40 -16
- package/dist/providers/openaiCompatible/client.js +36 -32
- package/dist/providers/perplexity.js +12 -21
- package/dist/providers/sagemaker/language-model.d.ts +2 -2
- package/dist/providers/togetherAi.js +12 -21
- package/dist/providers/xai.js +17 -26
- package/dist/server/errors.d.ts +1 -1
- package/dist/server/errors.js +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +5 -3
- package/dist/server/middleware/rateLimit.d.ts +0 -4
- package/dist/server/middleware/rateLimit.js +0 -4
- package/dist/types/errors.d.ts +35 -0
- package/dist/types/providers.d.ts +64 -0
- package/dist/utils/errorClassifier.d.ts +30 -0
- package/dist/utils/errorClassifier.js +93 -0
- package/dist/utils/fileDetector.js +6 -43
- package/dist/utils/providerHealth.d.ts +42 -7
- package/dist/utils/providerHealth.js +115 -122
- package/dist/utils/providerUtils.js +21 -63
- package/package.json +15 -70
|
@@ -2,13 +2,14 @@ import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
|
2
2
|
import { registerRuntimeContextWindow, registerRuntimeOutputCeiling, } from "../../constants/contextWindows.js";
|
|
3
3
|
import { createProxyFetch } from "../../proxy/proxyFetch.js";
|
|
4
4
|
import { AuthenticationError, InvalidModelError, ModelAccessDeniedError, NetworkError, ProviderError, RateLimitError, isModelAccessDeniedMessage, parseAllowedModels, } from "../../types/index.js";
|
|
5
|
+
import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../../utils/errorClassifier.js";
|
|
5
6
|
import { isAbortError } from "../../utils/errorHandling.js";
|
|
6
7
|
import { logger } from "../../utils/logger.js";
|
|
7
8
|
import { redactUrlCredentials } from "../../utils/logSanitize.js";
|
|
8
9
|
import { isGemini25Model as isCanonicalGemini25Model } from "../../utils/modelDetection.js";
|
|
9
10
|
import { calculateCost } from "../../utils/pricing.js";
|
|
10
11
|
import { getProviderModel } from "../../utils/providerConfig.js";
|
|
11
|
-
import { createTimeoutController
|
|
12
|
+
import { createTimeoutController } from "../../utils/timeout.js";
|
|
12
13
|
import { stripTrailingSlash } from "../openaiChatCompletionsClient.js";
|
|
13
14
|
import { OpenAIChatCompletionsProvider } from "../openaiChatCompletionsBase.js";
|
|
14
15
|
const streamTracer = trace.getTracer("neurolink.provider.litellm");
|
|
@@ -320,46 +321,60 @@ export class LiteLLMProvider extends OpenAIChatCompletionsProvider {
|
|
|
320
321
|
};
|
|
321
322
|
}
|
|
322
323
|
formatProviderError(error) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
324
|
+
// Curator P1-1: detect "team not allowed to access model" responses and
|
|
325
|
+
// surface as ModelAccessDeniedError with the allowed_models array parsed
|
|
326
|
+
// from the body. Must run before classification (not just before the
|
|
327
|
+
// "API key" rule) because ModelAccessDeniedError's constructor takes an
|
|
328
|
+
// `{ provider, requestedModel, allowedModels }` options object rather
|
|
329
|
+
// than the `(message, provider?)` shape ProviderErrorRule expects, so it
|
|
330
|
+
// can't be expressed as a declarative rule. No realistic overlap with the
|
|
331
|
+
// timeout/ECONNREFUSED checks below (disjoint wording), so running this
|
|
332
|
+
// first is behaviorally identical to the original nesting order.
|
|
326
333
|
const errorRecord = error;
|
|
327
|
-
if (errorRecord?.
|
|
328
|
-
(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
errorRecord.message.includes("Failed to fetch")) {
|
|
335
|
-
return new NetworkError("LiteLLM proxy server not available. Please start the LiteLLM proxy server at " +
|
|
336
|
-
`${process.env.LITELLM_BASE_URL || "http://localhost:4000"}`, this.providerName);
|
|
337
|
-
}
|
|
338
|
-
// Curator P1-1: detect "team not allowed to access model" responses and
|
|
339
|
-
// surface as ModelAccessDeniedError with the allowed_models array parsed
|
|
340
|
-
// from the body. Must run before the generic "API key" check because
|
|
341
|
-
// LiteLLM phrases this as a 403 distinct from auth.
|
|
342
|
-
if (isModelAccessDeniedMessage(errorRecord.message)) {
|
|
343
|
-
return new ModelAccessDeniedError(errorRecord.message, {
|
|
344
|
-
provider: this.providerName,
|
|
345
|
-
requestedModel: this.modelName,
|
|
346
|
-
allowedModels: parseAllowedModels(errorRecord.message),
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
if (errorRecord.message.includes("API_KEY_INVALID") ||
|
|
350
|
-
errorRecord.message.includes("Invalid API key")) {
|
|
351
|
-
return new AuthenticationError("Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.", this.providerName);
|
|
352
|
-
}
|
|
353
|
-
if (errorRecord.message.toLowerCase().includes("rate limit")) {
|
|
354
|
-
return new RateLimitError("LiteLLM rate limit exceeded. Please try again later.", this.providerName);
|
|
355
|
-
}
|
|
356
|
-
if (errorRecord.message.toLowerCase().includes("model") &&
|
|
357
|
-
errorRecord.message.toLowerCase().includes("not found")) {
|
|
358
|
-
return new InvalidModelError(`Model '${this.modelName}' not available in LiteLLM proxy. ` +
|
|
359
|
-
"Please check your LiteLLM configuration and ensure the model is configured.", this.providerName);
|
|
360
|
-
}
|
|
334
|
+
if (typeof errorRecord?.message === "string" &&
|
|
335
|
+
isModelAccessDeniedMessage(errorRecord.message)) {
|
|
336
|
+
return new ModelAccessDeniedError(errorRecord.message, {
|
|
337
|
+
provider: this.providerName,
|
|
338
|
+
requestedModel: this.modelName,
|
|
339
|
+
allowedModels: parseAllowedModels(errorRecord.message),
|
|
340
|
+
});
|
|
361
341
|
}
|
|
362
|
-
|
|
342
|
+
const rules = [
|
|
343
|
+
// Duck-typed timeout detection (name === "TimeoutError" OR message
|
|
344
|
+
// contains "timeout") distinct from the `instanceof TimeoutError` check
|
|
345
|
+
// classifyProviderError already performs first — preserved because
|
|
346
|
+
// some rejection paths produce a plain object/Error with that shape
|
|
347
|
+
// rather than a real TimeoutError instance.
|
|
348
|
+
{
|
|
349
|
+
match: (ctx) => ctx.errorName === "TimeoutError" || /timeout/i.test(ctx.message),
|
|
350
|
+
errorClass: NetworkError,
|
|
351
|
+
message: (ctx) => `Request timed out: ${ctx.message}`,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
match: (ctx) => /ECONNREFUSED|Failed to fetch/.test(ctx.message),
|
|
355
|
+
errorClass: NetworkError,
|
|
356
|
+
message: () => "LiteLLM proxy server not available. Please start the LiteLLM proxy server at " +
|
|
357
|
+
`${process.env.LITELLM_BASE_URL || "http://localhost:4000"}`,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
match: (ctx) => /API_KEY_INVALID|Invalid API key/.test(ctx.message),
|
|
361
|
+
errorClass: AuthenticationError,
|
|
362
|
+
message: "Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.",
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
match: (ctx) => /rate limit/i.test(ctx.message),
|
|
366
|
+
errorClass: RateLimitError,
|
|
367
|
+
message: "LiteLLM rate limit exceeded. Please try again later.",
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
match: (ctx) => /model/i.test(ctx.message) && /not found/i.test(ctx.message),
|
|
371
|
+
errorClass: InvalidModelError,
|
|
372
|
+
message: () => `Model '${this.modelName}' not available in LiteLLM proxy. ` +
|
|
373
|
+
"Please check your LiteLLM configuration and ensure the model is configured.",
|
|
374
|
+
},
|
|
375
|
+
...DEFAULT_ERROR_RULES,
|
|
376
|
+
];
|
|
377
|
+
return classifyProviderError(error, rules, this.providerName, this.modelName);
|
|
363
378
|
}
|
|
364
379
|
/**
|
|
365
380
|
* Get available models from LiteLLM proxy `/v1/models` endpoint.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NetworkError, ProviderError } from "../types/index.js";
|
|
2
|
+
import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../utils/errorClassifier.js";
|
|
2
3
|
import { logger } from "../utils/logger.js";
|
|
3
4
|
import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
4
|
-
import { TimeoutError } from "../utils/timeout.js";
|
|
5
5
|
import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
|
|
6
6
|
const LLAMACPP_DEFAULT_BASE_URL = "http://localhost:8080/v1";
|
|
7
7
|
const LLAMACPP_PLACEHOLDER_KEY = "llamacpp";
|
|
@@ -52,25 +52,28 @@ export class LlamaCppProvider extends OpenAIChatCompletionsProvider {
|
|
|
52
52
|
return ["loaded-model"];
|
|
53
53
|
}
|
|
54
54
|
formatProviderError(error) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
// `code`/`cause.code` aren't part of ProviderErrorContext, so they're read
|
|
56
|
+
// off the raw error here (mirrors ollama's `responseBody` extraction) for
|
|
57
|
+
// the ECONNREFUSED rule below, which the pre-migration code also checked
|
|
58
|
+
// via a duck-typed error code in addition to the message text.
|
|
58
59
|
const errorRecord = error;
|
|
59
|
-
const message = typeof errorRecord?.message === "string"
|
|
60
|
-
? errorRecord.message
|
|
61
|
-
: "Unknown error";
|
|
62
60
|
const cause = errorRecord?.cause ?? {};
|
|
63
61
|
const code = (errorRecord?.code ?? cause?.code);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
const rules = [
|
|
63
|
+
{
|
|
64
|
+
match: (ctx) => code === "ECONNREFUSED" ||
|
|
65
|
+
/ECONNREFUSED|Failed to fetch|fetch failed/.test(ctx.message),
|
|
66
|
+
errorClass: NetworkError,
|
|
67
|
+
message: () => `llama.cpp server not reachable at ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
68
|
+
"Start it with: ./llama-server -m model.gguf --port 8080",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
match: (ctx) => /400/.test(ctx.message),
|
|
72
|
+
errorClass: ProviderError,
|
|
73
|
+
message: "llama.cpp rejected the request. Common cause: model doesn't support tools (start llama-server with --jinja for tool support).",
|
|
74
|
+
},
|
|
75
|
+
...DEFAULT_ERROR_RULES,
|
|
76
|
+
];
|
|
77
|
+
return classifyProviderError(error, rules, "llamacpp", this.modelName);
|
|
75
78
|
}
|
|
76
79
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { InvalidModelError, NetworkError
|
|
1
|
+
import { InvalidModelError, NetworkError } from "../types/index.js";
|
|
2
|
+
import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../utils/errorClassifier.js";
|
|
2
3
|
import { logger } from "../utils/logger.js";
|
|
3
4
|
import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
4
|
-
import { TimeoutError } from "../utils/timeout.js";
|
|
5
5
|
import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
|
|
6
6
|
const LM_STUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1";
|
|
7
7
|
const LM_STUDIO_PLACEHOLDER_KEY = "lm-studio";
|
|
@@ -47,26 +47,29 @@ export class LMStudioProvider extends OpenAIChatCompletionsProvider {
|
|
|
47
47
|
return FALLBACK_MODEL;
|
|
48
48
|
}
|
|
49
49
|
formatProviderError(error) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
// `code`/`cause.code` aren't part of ProviderErrorContext, so they're read
|
|
51
|
+
// off the raw error here (mirrors ollama's `responseBody` extraction) for
|
|
52
|
+
// the ECONNREFUSED rule below, which the pre-migration code also checked
|
|
53
|
+
// via a duck-typed error code in addition to the message text.
|
|
53
54
|
const errorRecord = error;
|
|
54
|
-
const message = typeof errorRecord?.message === "string"
|
|
55
|
-
? errorRecord.message
|
|
56
|
-
: "Unknown error";
|
|
57
55
|
const cause = errorRecord?.cause ?? {};
|
|
58
56
|
const code = (errorRecord?.code ?? cause?.code);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
const rules = [
|
|
58
|
+
{
|
|
59
|
+
match: (ctx) => code === "ECONNREFUSED" ||
|
|
60
|
+
/ECONNREFUSED|Failed to fetch|fetch failed/.test(ctx.message),
|
|
61
|
+
errorClass: NetworkError,
|
|
62
|
+
message: () => `LM Studio server not reachable at ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
63
|
+
`Open the LM Studio app, load a model, and click "Start Server".`,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
match: (ctx) => /model_not_found|404/.test(ctx.message),
|
|
67
|
+
errorClass: InvalidModelError,
|
|
68
|
+
message: () => `LM Studio model '${this.modelName}' is not loaded. Load it in the LM Studio app first.`,
|
|
69
|
+
},
|
|
70
|
+
...DEFAULT_ERROR_RULES,
|
|
71
|
+
];
|
|
72
|
+
return classifyProviderError(error, rules, "lm-studio", this.modelName);
|
|
70
73
|
}
|
|
71
74
|
async validateConfiguration() {
|
|
72
75
|
return this.probeModelsEndpoint(this.config.apiKey && this.config.apiKey !== LM_STUDIO_PLACEHOLDER_KEY
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { MistralModels } from "../constants/enums.js";
|
|
2
|
-
import { AuthenticationError
|
|
2
|
+
import { AuthenticationError } from "../types/index.js";
|
|
3
3
|
import { logger } from "../utils/logger.js";
|
|
4
4
|
import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
5
|
+
import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../utils/errorClassifier.js";
|
|
5
6
|
import { createMistralConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
|
|
6
|
-
import { TimeoutError } from "../utils/timeout.js";
|
|
7
7
|
import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
|
|
8
8
|
const MISTRAL_DEFAULT_BASE_URL = "https://api.mistral.ai/v1";
|
|
9
9
|
const getMistralApiKey = () => {
|
|
@@ -60,28 +60,16 @@ export class MistralProvider extends OpenAIChatCompletionsProvider {
|
|
|
60
60
|
return getDefaultMistralModel();
|
|
61
61
|
}
|
|
62
62
|
formatProviderError(error) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
message.includes("401")) {
|
|
74
|
-
return new AuthenticationError("Invalid Mistral API key. Please check your MISTRAL_API_KEY environment variable.", "mistral");
|
|
75
|
-
}
|
|
76
|
-
if (message.includes("rate limit") ||
|
|
77
|
-
message.includes("Rate limit") ||
|
|
78
|
-
message.includes("429")) {
|
|
79
|
-
return new RateLimitError("Mistral rate limit exceeded", "mistral");
|
|
80
|
-
}
|
|
81
|
-
if (message.includes("model_not_found") || message.includes("404")) {
|
|
82
|
-
return new InvalidModelError(`Mistral model '${this.modelName}' not found.`, "mistral");
|
|
83
|
-
}
|
|
84
|
-
return new ProviderError(`Mistral error: ${message}`, "mistral");
|
|
63
|
+
const rules = [
|
|
64
|
+
{
|
|
65
|
+
match: (ctx) => ctx.statusCode === 401 ||
|
|
66
|
+
/API_KEY_INVALID|Invalid API key|Unauthorized/i.test(ctx.message),
|
|
67
|
+
errorClass: AuthenticationError,
|
|
68
|
+
message: "Invalid Mistral API key. Please check your MISTRAL_API_KEY environment variable.",
|
|
69
|
+
},
|
|
70
|
+
...DEFAULT_ERROR_RULES,
|
|
71
|
+
];
|
|
72
|
+
return classifyProviderError(error, rules, "mistral", this.modelName);
|
|
85
73
|
}
|
|
86
74
|
// ===========================================================================
|
|
87
75
|
// Optional hooks
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { NvidiaNimModels } from "../../constants/enums.js";
|
|
2
|
-
import { AuthenticationError, InvalidModelError,
|
|
2
|
+
import { AuthenticationError, InvalidModelError, ProviderError, RateLimitError, } from "../../types/index.js";
|
|
3
|
+
import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../../utils/errorClassifier.js";
|
|
3
4
|
import { logger } from "../../utils/logger.js";
|
|
4
5
|
import { redactUrlCredentials } from "../../utils/logSanitize.js";
|
|
5
6
|
import { createNvidiaNimConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
|
|
6
|
-
import { TimeoutError } from "../../utils/timeout.js";
|
|
7
7
|
import { OpenAIChatCompletionsProvider } from "../openaiChatCompletionsBase.js";
|
|
8
8
|
/**
|
|
9
9
|
* Decide whether a NIM 400 response body is a rejection of the named
|
|
@@ -237,38 +237,41 @@ export class NvidiaNimProvider extends OpenAIChatCompletionsProvider {
|
|
|
237
237
|
return JSON.parse(serialized);
|
|
238
238
|
}
|
|
239
239
|
formatProviderError(error) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
240
|
+
const rules = [
|
|
241
|
+
// NIM canonically returns HTTP 401/Unauthorized for invalid API keys,
|
|
242
|
+
// but its OpenAI-compatible gateway sometimes surfaces a bare 400 +
|
|
243
|
+
// "Bad Request" with no body details for both malformed-credentials
|
|
244
|
+
// and bad-parameter cases. Because the two are indistinguishable from
|
|
245
|
+
// the message alone, bare 400/"Bad Request" is deliberately NOT
|
|
246
|
+
// promoted to "invalid key" here — that would mis-classify legitimate
|
|
247
|
+
// parameter errors (e.g. unsupported `reasoning_budget`, unsupported
|
|
248
|
+
// `chat_template`) as auth failures. Tests that probe the auth path
|
|
249
|
+
// (K1) detect "bad request" / "400" themselves; tests that probe
|
|
250
|
+
// parameter retry (K5) need the original "Bad Request" message to
|
|
251
|
+
// surface.
|
|
252
|
+
{
|
|
253
|
+
match: (ctx) => /Invalid API key|401|Unauthorized/.test(ctx.message),
|
|
254
|
+
errorClass: AuthenticationError,
|
|
255
|
+
message: "Invalid NVIDIA NIM API key. Get one at https://build.nvidia.com/settings/api-keys",
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
match: (ctx) => /rate limit|429/.test(ctx.message),
|
|
259
|
+
errorClass: RateLimitError,
|
|
260
|
+
message: "NVIDIA NIM rate limit exceeded",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
match: (ctx) => /404|model_not_found/.test(ctx.message),
|
|
264
|
+
errorClass: InvalidModelError,
|
|
265
|
+
message: () => `NVIDIA NIM model '${this.modelName}' not available. Browse the catalog at https://build.nvidia.com/models`,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
match: (ctx) => /quota|403/.test(ctx.message),
|
|
269
|
+
errorClass: ProviderError,
|
|
270
|
+
message: "NVIDIA NIM quota exceeded for your account",
|
|
271
|
+
},
|
|
272
|
+
...DEFAULT_ERROR_RULES,
|
|
273
|
+
];
|
|
274
|
+
return classifyProviderError(error, rules, "nvidia-nim", this.modelName);
|
|
272
275
|
}
|
|
273
276
|
async validateConfiguration() {
|
|
274
277
|
return (typeof this.config.apiKey === "string" &&
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { modelConfig } from "../../core/modelConfiguration.js";
|
|
2
2
|
import { createProxyFetch } from "../../proxy/proxyFetch.js";
|
|
3
3
|
import { InvalidModelError, NetworkError, ProviderError, } from "../../types/index.js";
|
|
4
|
+
import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../../utils/errorClassifier.js";
|
|
4
5
|
import { logger } from "../../utils/logger.js";
|
|
5
6
|
import { redactUrlCredentials } from "../../utils/logSanitize.js";
|
|
6
7
|
import { createTimeoutController, parseTimeout, TimeoutError, } from "../../utils/timeout.js";
|
|
@@ -81,47 +82,61 @@ export class OllamaProvider extends OpenAIChatCompletionsProvider {
|
|
|
81
82
|
}
|
|
82
83
|
formatProviderError(error) {
|
|
83
84
|
if (error instanceof TimeoutError) {
|
|
85
|
+
// Custom message (not classifyProviderError's built-in "Request timed
|
|
86
|
+
// out: ..." default). TimeoutError is handled unconditionally inside
|
|
87
|
+
// classifyProviderError ahead of any rule table, so this quirk can only
|
|
88
|
+
// be preserved via a pre-delegate intercept (same pattern used for
|
|
89
|
+
// groq's TimeoutError override).
|
|
84
90
|
return new NetworkError(`Ollama request timed out. The model may be loading or the request is too large.`, "ollama");
|
|
85
91
|
}
|
|
92
|
+
// `responseBody` isn't part of ProviderErrorContext, so it's read off the
|
|
93
|
+
// raw error here (mirrors openAI's `errorType` extraction) for the
|
|
94
|
+
// missing-model / 404 rules below, which match against message+body
|
|
95
|
+
// combined exactly as the pre-migration code did.
|
|
86
96
|
const errorRecord = error;
|
|
87
|
-
const message = typeof errorRecord?.message === "string"
|
|
88
|
-
? errorRecord.message
|
|
89
|
-
: "Unknown error";
|
|
90
|
-
const cause = errorRecord?.cause ?? {};
|
|
91
|
-
const code = (errorRecord?.code ?? cause?.code);
|
|
92
|
-
if (code === "ECONNREFUSED" ||
|
|
93
|
-
message.includes("ECONNREFUSED") ||
|
|
94
|
-
message.includes("Failed to fetch") ||
|
|
95
|
-
message.includes("fetch failed")) {
|
|
96
|
-
return new NetworkError(`Cannot connect to Ollama at ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
97
|
-
`Install Ollama (https://ollama.com), start it with 'ollama serve', then try again.`, "ollama");
|
|
98
|
-
}
|
|
99
|
-
// The base client (buildAPIError) attaches statusCode + responseBody to
|
|
100
|
-
// HTTP failures. Distinguish a genuine missing-model error (give 'ollama
|
|
101
|
-
// pull' guidance) from a bare endpoint-mismatch 404 (wrong base URL / not
|
|
102
|
-
// the OpenAI-compatible /v1 surface) so the advice is actionable. Match on
|
|
103
|
-
// wording, not a bare "404" substring, to avoid misclassifying unrelated
|
|
104
|
-
// messages that merely contain those digits.
|
|
105
|
-
const statusCode = typeof errorRecord?.statusCode === "number"
|
|
106
|
-
? errorRecord.statusCode
|
|
107
|
-
: undefined;
|
|
108
97
|
const responseBody = typeof errorRecord?.responseBody === "string"
|
|
109
98
|
? errorRecord.responseBody
|
|
110
99
|
: "";
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
100
|
+
const cause = errorRecord?.cause ?? {};
|
|
101
|
+
const code = (errorRecord?.code ?? cause?.code);
|
|
102
|
+
const rules = [
|
|
103
|
+
{
|
|
104
|
+
match: (ctx) => code === "ECONNREFUSED" ||
|
|
105
|
+
/ECONNREFUSED|Failed to fetch|fetch failed/.test(ctx.message),
|
|
106
|
+
errorClass: NetworkError,
|
|
107
|
+
message: () => `Cannot connect to Ollama at ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
108
|
+
`Install Ollama (https://ollama.com), start it with 'ollama serve', then try again.`,
|
|
109
|
+
},
|
|
110
|
+
// The base client (buildAPIError) attaches statusCode + responseBody to
|
|
111
|
+
// HTTP failures. Distinguish a genuine missing-model error (give 'ollama
|
|
112
|
+
// pull' guidance) from a bare endpoint-mismatch 404 (wrong base URL / not
|
|
113
|
+
// the OpenAI-compatible /v1 surface) so the advice is actionable. Match
|
|
114
|
+
// on wording, not a bare "404" substring, to avoid misclassifying
|
|
115
|
+
// unrelated messages that merely contain those digits.
|
|
116
|
+
{
|
|
117
|
+
match: (ctx) => {
|
|
118
|
+
const haystack = `${ctx.message} ${responseBody}`.toLowerCase();
|
|
119
|
+
return (haystack.includes("model_not_found") ||
|
|
120
|
+
(haystack.includes("model") && haystack.includes("not found")));
|
|
121
|
+
},
|
|
122
|
+
errorClass: InvalidModelError,
|
|
123
|
+
message: () => `Ollama model '${this.modelName}' is not available locally. ` +
|
|
124
|
+
`Pull it first with 'ollama pull ${this.modelName}' (or try '${FALLBACK_OLLAMA_MODEL}'); ` +
|
|
125
|
+
`list installed models with 'ollama list'.`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
match: (ctx) => {
|
|
129
|
+
const haystack = `${ctx.message} ${responseBody}`.toLowerCase();
|
|
130
|
+
return ctx.statusCode === 404 || haystack.includes("status 404");
|
|
131
|
+
},
|
|
132
|
+
errorClass: ProviderError,
|
|
133
|
+
message: () => `Ollama returned HTTP 404 from ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
134
|
+
`Verify the base URL serves the OpenAI-compatible /v1 API and that the ` +
|
|
135
|
+
`model is installed ('ollama list').`,
|
|
136
|
+
},
|
|
137
|
+
...DEFAULT_ERROR_RULES,
|
|
138
|
+
];
|
|
139
|
+
return classifyProviderError(error, rules, "ollama", this.modelName);
|
|
125
140
|
}
|
|
126
141
|
// ===========================================================================
|
|
127
142
|
// Optional hooks — Ollama-specific behaviour
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
2
|
import { AIProviderName as AIProviderNameEnum } from "../../constants/enums.js";
|
|
3
3
|
import { createProxyFetch } from "../../proxy/proxyFetch.js";
|
|
4
|
-
import { AuthenticationError, InvalidModelError,
|
|
4
|
+
import { AuthenticationError, InvalidModelError, ProviderError, RateLimitError, } from "../../types/index.js";
|
|
5
5
|
import { logger } from "../../utils/logger.js";
|
|
6
6
|
import { redactUrlCredentials } from "../../utils/logSanitize.js";
|
|
7
7
|
import { calculateCost } from "../../utils/pricing.js";
|
|
8
|
+
import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../../utils/errorClassifier.js";
|
|
8
9
|
import { createOpenAIConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
|
|
9
10
|
import { MAX_IMAGE_BYTES, readBoundedBuffer } from "../../utils/sizeGuard.js";
|
|
10
11
|
import { assertSafeUrl } from "../../utils/ssrfGuard.js";
|
|
11
|
-
import { createTimeoutController
|
|
12
|
+
import { createTimeoutController } from "../../utils/timeout.js";
|
|
12
13
|
import { stripTrailingSlash } from "../openaiChatCompletionsClient.js";
|
|
13
14
|
import { OpenAIChatCompletionsProvider } from "../openaiChatCompletionsBase.js";
|
|
14
15
|
const OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
@@ -89,48 +90,44 @@ export class OpenAIProvider extends OpenAIChatCompletionsProvider {
|
|
|
89
90
|
return getOpenAIModel();
|
|
90
91
|
}
|
|
91
92
|
formatProviderError(error) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
// `type` isn't part of `ProviderErrorContext` (it's an OpenAI-specific
|
|
94
|
+
// error-body field), so it's read directly off the raw error here and
|
|
95
|
+
// captured by the rule closures below.
|
|
95
96
|
const errorObj = error;
|
|
96
|
-
const message = errorObj?.message && typeof errorObj.message === "string"
|
|
97
|
-
? errorObj.message
|
|
98
|
-
: "Unknown error";
|
|
99
97
|
const errorType = errorObj?.type && typeof errorObj.type === "string"
|
|
100
98
|
? errorObj.type
|
|
101
99
|
: undefined;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return new ProviderError(`OpenAI error: ${message}`, this.providerName);
|
|
100
|
+
const rules = [
|
|
101
|
+
// Curator P1-1 / Reviewer Finding #4: only the explicit auth markers
|
|
102
|
+
// map to AuthenticationError. Earlier we treated every
|
|
103
|
+
// `invalid_request_error` as an auth failure — that's OpenAI's
|
|
104
|
+
// catch-all for any bad request (unsupported parameter, malformed
|
|
105
|
+
// JSON, etc.) and mislabelled them as "invalid API key". Use
|
|
106
|
+
// credential-specific signals only.
|
|
107
|
+
{
|
|
108
|
+
match: (ctx) => ctx.statusCode === 401 ||
|
|
109
|
+
errorType === "invalid_api_key" ||
|
|
110
|
+
/API_KEY_INVALID|Invalid API key|Incorrect API key|invalid_api_key/i.test(ctx.message),
|
|
111
|
+
errorClass: AuthenticationError,
|
|
112
|
+
message: (ctx) => /Incorrect API key|Invalid API key/i.test(ctx.message)
|
|
113
|
+
? ctx.message
|
|
114
|
+
: "Invalid OpenAI API key. Please check your OPENAI_API_KEY environment variable.",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
match: (ctx) => ctx.statusCode === 429 ||
|
|
118
|
+
errorType === "rate_limit_error" ||
|
|
119
|
+
/rate limit/i.test(ctx.message),
|
|
120
|
+
errorClass: RateLimitError,
|
|
121
|
+
message: "OpenAI rate limit exceeded. Please try again later.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
match: (ctx) => /model_not_found/i.test(ctx.message),
|
|
125
|
+
errorClass: InvalidModelError,
|
|
126
|
+
message: (ctx) => `Model not found: ${ctx.modelName}`,
|
|
127
|
+
},
|
|
128
|
+
...DEFAULT_ERROR_RULES,
|
|
129
|
+
];
|
|
130
|
+
return classifyProviderError(error, rules, this.providerName, this.modelName);
|
|
134
131
|
}
|
|
135
132
|
// ===========================================================================
|
|
136
133
|
// Optional hook overrides
|