@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
|
@@ -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
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AIProviderName } from "../../constants/enums.js";
|
|
2
2
|
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
|
|
3
3
|
import { createProxyFetch } from "../../proxy/proxyFetch.js";
|
|
4
|
+
import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../../utils/errorClassifier.js";
|
|
4
5
|
import { isAbortError } from "../../utils/errorHandling.js";
|
|
5
6
|
import { logger } from "../../utils/logger.js";
|
|
6
7
|
import { redactUrlCredentials } from "../../utils/logSanitize.js";
|
|
7
|
-
import { TimeoutError } from "../../utils/timeout.js";
|
|
8
8
|
import { OpenAIChatCompletionsProvider } from "../openaiChatCompletionsBase.js";
|
|
9
9
|
import { stripTrailingSlash } from "../openaiChatCompletionsClient.js";
|
|
10
10
|
import { getDefaultOpenRouterModel } from "./utils.js";
|
|
@@ -87,63 +87,69 @@ export class OpenRouterProvider extends OpenAIChatCompletionsProvider {
|
|
|
87
87
|
return getDefaultOpenRouterModel();
|
|
88
88
|
}
|
|
89
89
|
formatProviderError(error) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
90
|
+
const rules = [
|
|
91
|
+
// Duck-typed timeout detection (name === "TimeoutError" OR message
|
|
92
|
+
// contains "Timeout", case-sensitive — matches the original's
|
|
93
|
+
// `.includes("Timeout")`) distinct from the `instanceof TimeoutError`
|
|
94
|
+
// check classifyProviderError already performs first.
|
|
95
|
+
{
|
|
96
|
+
match: (ctx) => ctx.errorName === "TimeoutError" || /Timeout/.test(ctx.message),
|
|
97
|
+
errorClass: NetworkError,
|
|
98
|
+
message: (ctx) => `Request timed out: ${ctx.message}`,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
match: (ctx) => /ECONNREFUSED|Failed to fetch/.test(ctx.message),
|
|
102
|
+
errorClass: NetworkError,
|
|
103
|
+
message: "OpenRouter API not available. Please check your network connection and try again.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
match: (ctx) => /API_KEY_INVALID|Invalid API key|invalid_api_key|Unauthorized/.test(ctx.message),
|
|
107
|
+
errorClass: AuthenticationError,
|
|
108
|
+
message: "Invalid OpenRouter API key. Please check your OPENROUTER_API_KEY environment variable. " +
|
|
109
|
+
"Get your key at https://openrouter.ai/keys",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
match: (ctx) => /rate limit/.test(ctx.message),
|
|
113
|
+
errorClass: RateLimitError,
|
|
114
|
+
message: "OpenRouter rate limit exceeded. Please try again later or upgrade your account at https://openrouter.ai/credits",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
match: (ctx) => /model/.test(ctx.message) && /not found/.test(ctx.message),
|
|
118
|
+
errorClass: InvalidModelError,
|
|
119
|
+
message: () => `Model '${this.modelName}' not available on OpenRouter. ` +
|
|
120
|
+
"Browse available models at https://openrouter.ai/models",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
match: (ctx) => /insufficient_credits/.test(ctx.message),
|
|
124
|
+
errorClass: ProviderError,
|
|
125
|
+
message: "Insufficient OpenRouter credits. Add credits at https://openrouter.ai/credits",
|
|
126
|
+
},
|
|
123
127
|
// "No endpoints found" — model temporarily unavailable or unsupported
|
|
124
128
|
// parameters. Distinct from tool errors: it can happen on any request
|
|
125
129
|
// when the model has no available providers on OpenRouter.
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
{
|
|
131
|
+
match: (ctx) => /No endpoints found/.test(ctx.message),
|
|
132
|
+
errorClass: InvalidModelError,
|
|
133
|
+
message: () => `No endpoints found for model '${this.modelName}' on OpenRouter. ` +
|
|
128
134
|
"The model may be temporarily unavailable or does not support the requested parameters. " +
|
|
129
|
-
"Try a different model or check availability at https://openrouter.ai/models",
|
|
130
|
-
}
|
|
135
|
+
"Try a different model or check availability at https://openrouter.ai/models",
|
|
136
|
+
},
|
|
131
137
|
// Tool/function calling errors
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return new ProviderError(`Model '${this.modelName}' does not support tool calling. ` +
|
|
138
|
+
{
|
|
139
|
+
match: (ctx) => /tool use|tool_use|function_call|tools are not supported/.test(ctx.message),
|
|
140
|
+
errorClass: ProviderError,
|
|
141
|
+
message: () => `Model '${this.modelName}' does not support tool calling. ` +
|
|
137
142
|
"Use a tool-capable model like:\n" +
|
|
138
143
|
" • google/gemini-2.0-flash-exp:free (free)\n" +
|
|
139
144
|
" • meta-llama/llama-3.3-70b-instruct:free (free)\n" +
|
|
140
145
|
" • anthropic/claude-3.7-sonnet (paid)\n" +
|
|
141
146
|
" • openai/gpt-4o (paid)\n" +
|
|
142
147
|
"Or use --disableTools flag. " +
|
|
143
|
-
"See all tool-capable models at https://openrouter.ai/models?supported_parameters=tools",
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
"See all tool-capable models at https://openrouter.ai/models?supported_parameters=tools",
|
|
149
|
+
},
|
|
150
|
+
...DEFAULT_ERROR_RULES,
|
|
151
|
+
];
|
|
152
|
+
return classifyProviderError(error, rules, "openrouter", this.modelName);
|
|
147
153
|
}
|
|
148
154
|
// ===========================================================================
|
|
149
155
|
// Optional hooks — provider-specific quirks
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* Nothing here imports from "ai" or "@ai-sdk/*". The base class is a
|
|
18
18
|
* direct HTTP client + multi-step tool-execution loop driven by SSE.
|
|
19
19
|
*/
|
|
20
|
+
import { trace } from "@opentelemetry/api";
|
|
20
21
|
import { getAvailableInputTokens, getRuntimeContextWindow, getRuntimeOutputCeiling, registerRuntimeContextWindow, } from "../constants/contextWindows.js";
|
|
21
22
|
import { guardOpenAICompatConversation } from "../context/openaiCompatLoopGuard.js";
|
|
22
23
|
import { isContextOverflowError, parseProviderOverflowDetails, } from "../context/errorDetection.js";
|
|
@@ -33,6 +34,7 @@ import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals,
|
|
|
33
34
|
import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
|
|
34
35
|
import { resolveToolChoice } from "../utils/toolChoice.js";
|
|
35
36
|
import { transformToolExecutions } from "../utils/transformationUtils.js";
|
|
37
|
+
import { withProviderRetry } from "../utils/providerRetry.js";
|
|
36
38
|
import { resolveDeferredTool } from "../tools/toolDiscovery.js";
|
|
37
39
|
import { buildAPIError, buildBody, buildToolsForOpenAI, buildWireToolNameMaps, createChunkQueue, createDeferredAnalytics, ensureJsonWordInBody, estimateWireTokens, mapNeuroLinkToolChoice, mergeUsage, messageBuilderToOpenAI, parseSSEStream, stringifyToolOutput, stripTrailingSlash, v3ResponseFormatToOpenAI, v3ToolChoiceToOpenAI, v3ToolsToOpenAI, } from "./openaiChatCompletionsClient.js";
|
|
38
40
|
/**
|
|
@@ -932,22 +934,44 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
932
934
|
: {}),
|
|
933
935
|
streaming: true,
|
|
934
936
|
}), args.modelId));
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
937
|
+
// The initial fetch gets 429/5xx retry-with-backoff via the same
|
|
938
|
+
// primitive the non-streaming path already uses (withProviderRetry).
|
|
939
|
+
// `doFetch` throws the classified APIError (buildAPIError attaches
|
|
940
|
+
// .statusCode + .responseHeaders, which withProviderRetry's duck-typing
|
|
941
|
+
// reads directly) so a non-ok response is what drives the retry
|
|
942
|
+
// decision, not a return value.
|
|
943
|
+
const doFetch = async () => {
|
|
944
|
+
const attemptRes = await args.fetchImpl(args.url, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
headers: {
|
|
947
|
+
"Content-Type": "application/json",
|
|
948
|
+
...this.getAuthHeaders(),
|
|
949
|
+
},
|
|
950
|
+
body: JSON.stringify(body),
|
|
951
|
+
...(args.abortSignal ? { signal: args.abortSignal } : {}),
|
|
952
|
+
});
|
|
953
|
+
if (!attemptRes.ok) {
|
|
954
|
+
throw await buildAPIError(args.url, body, attemptRes);
|
|
955
|
+
}
|
|
956
|
+
return attemptRes;
|
|
957
|
+
};
|
|
958
|
+
let res;
|
|
959
|
+
try {
|
|
960
|
+
res = await withProviderRetry(doFetch, trace.getActiveSpan() ?? undefined, `${this.providerName} stream`);
|
|
961
|
+
}
|
|
962
|
+
catch (err) {
|
|
963
|
+
// The one-shot 400 context-overflow fallback lives outside
|
|
964
|
+
// withProviderRetry (400 isn't retryable there anyway — see
|
|
965
|
+
// isRetryableProviderError), so it fires on the classified error's
|
|
966
|
+
// .statusCode. The raw Response is no longer in scope here: it was
|
|
967
|
+
// consumed inside doFetch's closure, either returned on success or
|
|
968
|
+
// discarded after buildAPIError read its body on failure.
|
|
969
|
+
const apiErr = err;
|
|
970
|
+
// Overflow corrector first (re-fits max_tokens from the provider's
|
|
971
|
+
// own numbers + self-heals the window registry), then the subclass
|
|
972
|
+
// hook (e.g. NIM strips chat_template / reasoning_budget when a model
|
|
973
|
+
// rejects them).
|
|
974
|
+
const retryBody = apiErr.statusCode === 400
|
|
951
975
|
? (this.correctBodyAfterContextOverflow(body, apiErr) ??
|
|
952
976
|
this.adjustBodyAfter400(body, apiErr))
|
|
953
977
|
: undefined;
|