@juspay/neurolink 11.1.0 → 11.2.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 +15 -0
- package/dist/browser/neurolink.min.js +367 -367
- package/dist/cli/commands/setup.d.ts +4 -1
- package/dist/cli/commands/setup.js +44 -14
- package/dist/constants/networkErrorCodes.d.ts +14 -0
- package/dist/constants/networkErrorCodes.js +21 -0
- package/dist/factories/providerDescriptors.js +13 -3
- package/dist/lib/constants/networkErrorCodes.d.ts +14 -0
- package/dist/lib/constants/networkErrorCodes.js +22 -0
- package/dist/lib/factories/providerDescriptors.js +13 -3
- package/dist/lib/processors/base/BaseFileProcessor.d.ts +17 -0
- package/dist/lib/processors/base/BaseFileProcessor.js +40 -0
- package/dist/lib/processors/document/OpenDocumentProcessor.js +12 -2
- package/dist/lib/providers/configuredOpenAICompat.d.ts +24 -0
- package/dist/lib/providers/configuredOpenAICompat.js +60 -0
- package/dist/lib/providers/openaiCompatCatalog.d.ts +24 -0
- package/dist/lib/providers/openaiCompatCatalog.js +272 -0
- package/dist/lib/proxy/proxyFetch.js +1 -9
- package/dist/lib/types/cli.d.ts +2 -0
- package/dist/lib/types/providers.d.ts +116 -2
- package/dist/lib/utils/errorClassifier.js +100 -11
- package/dist/lib/utils/providerConfig.d.ts +39 -1
- package/dist/lib/utils/providerConfig.js +83 -0
- package/dist/lib/utils/providerHealth.d.ts +30 -24
- package/dist/lib/utils/providerHealth.js +42 -41
- package/dist/lib/utils/providerUtils.js +2 -2
- package/dist/processors/base/BaseFileProcessor.d.ts +17 -0
- package/dist/processors/base/BaseFileProcessor.js +40 -0
- package/dist/processors/document/OpenDocumentProcessor.js +12 -2
- package/dist/providers/configuredOpenAICompat.d.ts +24 -0
- package/dist/providers/configuredOpenAICompat.js +59 -0
- package/dist/providers/openaiCompatCatalog.d.ts +24 -0
- package/dist/providers/openaiCompatCatalog.js +271 -0
- package/dist/proxy/proxyFetch.js +1 -9
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/providers.d.ts +116 -2
- package/dist/utils/errorClassifier.js +100 -11
- package/dist/utils/providerConfig.d.ts +39 -1
- package/dist/utils/providerConfig.js +83 -0
- package/dist/utils/providerHealth.d.ts +30 -24
- package/dist/utils/providerHealth.js +42 -41
- package/dist/utils/providerUtils.js +2 -2
- package/package.json +2 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { AIProviderName } from "../constants/enums.js";
|
|
2
|
+
import { CloudflareModels, FireworksModels, GroqModels, MistralModels, PerplexityModels, TogetherAIModels, XaiModels, } from "../constants/enums.js";
|
|
3
|
+
import { AuthenticationError, InvalidModelError, ProviderError, } from "../types/index.js";
|
|
4
|
+
import { DEFAULT_ERROR_RULES } from "../utils/errorClassifier.js";
|
|
5
|
+
import { createCloudflareConfig, createFireworksConfig, createGroqConfig, createMistralConfig, createPerplexityConfig, createTogetherAIConfig, createXaiConfig, } from "../utils/providerConfig.js";
|
|
6
|
+
function buildCloudflareBaseURL(accountId) {
|
|
7
|
+
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Config-driven catalog of the 7 zero-quirk OpenAI-compatible providers.
|
|
11
|
+
* Each entry fully replaces what used to be a hand-written
|
|
12
|
+
* OpenAIChatCompletionsProvider subclass — see ConfiguredOpenAICompatProvider
|
|
13
|
+
* for the class that reads these entries, and providerRegistry.ts for the
|
|
14
|
+
* registration loop that consumes this array.
|
|
15
|
+
*
|
|
16
|
+
* `errorRules` mirrors each provider's LIVE `formatProviderError` rule array
|
|
17
|
+
* (post plan-07/wave-2 migration), not the original hand-rolled ladder these
|
|
18
|
+
* providers had when plan 05 was first drafted: every provider below now
|
|
19
|
+
* keeps only its bespoke rule(s) — auth, plus Groq's model_decommissioned and
|
|
20
|
+
* xAI's insufficient_quota — before spreading the SAME exported
|
|
21
|
+
* `DEFAULT_ERROR_RULES` constant that the live subclasses spread (never an
|
|
22
|
+
* inlined copy, so this catalog cannot drift from that table independently).
|
|
23
|
+
* See plan-05/progress.md Ruling R4 for the full rationale.
|
|
24
|
+
*
|
|
25
|
+
* To add a new zero-quirk OpenAI-compatible provider: add one entry here.
|
|
26
|
+
* Do NOT add a provider here if it needs any hook override beyond the 3
|
|
27
|
+
* mandatory ones (getProviderName/getDefaultModel/formatProviderError) —
|
|
28
|
+
* write a dedicated subclass instead (see deepseek.ts, azureOpenai.ts, and
|
|
29
|
+
* Task 14's docs task for the deciding criteria).
|
|
30
|
+
*/
|
|
31
|
+
export const OPENAI_COMPAT_CATALOG = [
|
|
32
|
+
{
|
|
33
|
+
providerName: AIProviderName.GROQ,
|
|
34
|
+
aliases: ["groq"],
|
|
35
|
+
apiKeyEnvVar: "GROQ_API_KEY",
|
|
36
|
+
baseURLEnvVar: "GROQ_BASE_URL",
|
|
37
|
+
defaultBaseURL: "https://api.groq.com/openai/v1",
|
|
38
|
+
configOptions: createGroqConfig(),
|
|
39
|
+
modelEnvVar: "GROQ_MODEL",
|
|
40
|
+
defaultModel: GroqModels.LLAMA_3_3_70B_VERSATILE,
|
|
41
|
+
registryDefaultModel: GroqModels.LLAMA_3_3_70B_VERSATILE,
|
|
42
|
+
registryDefaultModelChecksEnvVar: true,
|
|
43
|
+
fallbackModelName: GroqModels.LLAMA_3_1_8B_INSTANT,
|
|
44
|
+
fallbackModels: [
|
|
45
|
+
GroqModels.LLAMA_3_3_70B_VERSATILE,
|
|
46
|
+
GroqModels.LLAMA_3_1_8B_INSTANT,
|
|
47
|
+
GroqModels.GEMMA_2_9B_IT,
|
|
48
|
+
GroqModels.MIXTRAL_8X7B_32768,
|
|
49
|
+
GroqModels.LLAMA_3_2_90B_VISION_PREVIEW,
|
|
50
|
+
GroqModels.LLAMA_3_2_11B_VISION_PREVIEW,
|
|
51
|
+
],
|
|
52
|
+
// KNOWN GAP (not reproducible by this data-only array — flagged for PR C,
|
|
53
|
+
// see plan-05/task-A-report.md): live GroqProvider.formatProviderError()
|
|
54
|
+
// intercepts TimeoutError and returns a plain ProviderError BEFORE ever
|
|
55
|
+
// calling classifyProviderError, overriding that function's own
|
|
56
|
+
// (non-overridable) rule that TimeoutError always maps to NetworkError.
|
|
57
|
+
// ConfiguredOpenAICompatProvider.formatProviderError() has no such
|
|
58
|
+
// pre-check hook, so migrating Groq onto it as-is would silently
|
|
59
|
+
// reclassify Groq timeouts as NetworkError. This entry's errorRules
|
|
60
|
+
// still mirrors Groq's live rule array faithfully for every other error
|
|
61
|
+
// shape; the TimeoutError special case is a structural gap in
|
|
62
|
+
// ConfiguredOpenAICompatProvider, not a data error here.
|
|
63
|
+
errorRules: [
|
|
64
|
+
{
|
|
65
|
+
match: (ctx) => ctx.statusCode === 401 ||
|
|
66
|
+
/Invalid API key|Authentication|invalid_api_key/i.test(ctx.message),
|
|
67
|
+
errorClass: AuthenticationError,
|
|
68
|
+
message: "Invalid Groq API key. Check GROQ_API_KEY. Get one at https://console.groq.com/keys",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
match: (ctx) => /model_decommissioned/i.test(ctx.message),
|
|
72
|
+
errorClass: InvalidModelError,
|
|
73
|
+
message: (ctx) => `Groq model '${ctx.modelName}' was decommissioned. Pick a current model from https://console.groq.com/docs/models.`,
|
|
74
|
+
},
|
|
75
|
+
...DEFAULT_ERROR_RULES,
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
providerName: AIProviderName.XAI,
|
|
80
|
+
aliases: ["xai", "grok"],
|
|
81
|
+
apiKeyEnvVar: "XAI_API_KEY",
|
|
82
|
+
baseURLEnvVar: "XAI_BASE_URL",
|
|
83
|
+
defaultBaseURL: "https://api.x.ai/v1",
|
|
84
|
+
configOptions: createXaiConfig(),
|
|
85
|
+
modelEnvVar: "XAI_MODEL",
|
|
86
|
+
defaultModel: XaiModels.GROK_3,
|
|
87
|
+
registryDefaultModel: XaiModels.GROK_3,
|
|
88
|
+
registryDefaultModelChecksEnvVar: true,
|
|
89
|
+
fallbackModelName: XaiModels.GROK_3_MINI,
|
|
90
|
+
fallbackModels: [
|
|
91
|
+
XaiModels.GROK_3,
|
|
92
|
+
XaiModels.GROK_3_MINI,
|
|
93
|
+
XaiModels.GROK_2_LATEST,
|
|
94
|
+
XaiModels.GROK_2_VISION_LATEST,
|
|
95
|
+
XaiModels.GROK_BETA,
|
|
96
|
+
],
|
|
97
|
+
errorRules: [
|
|
98
|
+
{
|
|
99
|
+
match: (ctx) => ctx.statusCode === 401 ||
|
|
100
|
+
/Invalid API key|Authentication|invalid_api_key/i.test(ctx.message),
|
|
101
|
+
errorClass: AuthenticationError,
|
|
102
|
+
message: "Invalid xAI API key. Please check your XAI_API_KEY environment variable. Get one at https://console.x.ai/",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
match: (ctx) => /insufficient_quota|quota exceeded/i.test(ctx.message),
|
|
106
|
+
errorClass: ProviderError,
|
|
107
|
+
message: "xAI account has insufficient quota. Top up at https://console.x.ai/",
|
|
108
|
+
},
|
|
109
|
+
...DEFAULT_ERROR_RULES,
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
providerName: AIProviderName.TOGETHER_AI,
|
|
114
|
+
aliases: ["together-ai", "together"],
|
|
115
|
+
apiKeyEnvVar: "TOGETHER_API_KEY",
|
|
116
|
+
baseURLEnvVar: "TOGETHER_BASE_URL",
|
|
117
|
+
defaultBaseURL: "https://api.together.xyz/v1",
|
|
118
|
+
configOptions: createTogetherAIConfig(),
|
|
119
|
+
modelEnvVar: "TOGETHER_MODEL",
|
|
120
|
+
defaultModel: TogetherAIModels.LLAMA_3_3_70B_INSTRUCT_TURBO,
|
|
121
|
+
registryDefaultModel: TogetherAIModels.LLAMA_3_3_70B_INSTRUCT_TURBO,
|
|
122
|
+
registryDefaultModelChecksEnvVar: true,
|
|
123
|
+
fallbackModelName: TogetherAIModels.LLAMA_3_1_8B_INSTRUCT_TURBO,
|
|
124
|
+
fallbackModels: [
|
|
125
|
+
TogetherAIModels.LLAMA_3_3_70B_INSTRUCT_TURBO,
|
|
126
|
+
TogetherAIModels.LLAMA_3_1_405B_INSTRUCT_TURBO,
|
|
127
|
+
TogetherAIModels.LLAMA_3_1_70B_INSTRUCT_TURBO,
|
|
128
|
+
TogetherAIModels.LLAMA_3_1_8B_INSTRUCT_TURBO,
|
|
129
|
+
TogetherAIModels.MIXTRAL_8X22B_INSTRUCT,
|
|
130
|
+
TogetherAIModels.QWEN_2_5_72B_INSTRUCT_TURBO,
|
|
131
|
+
TogetherAIModels.DEEPSEEK_R1,
|
|
132
|
+
TogetherAIModels.DEEPSEEK_V3,
|
|
133
|
+
],
|
|
134
|
+
errorRules: [
|
|
135
|
+
{
|
|
136
|
+
match: (ctx) => ctx.statusCode === 401 ||
|
|
137
|
+
/Invalid API key|Authentication/i.test(ctx.message),
|
|
138
|
+
errorClass: AuthenticationError,
|
|
139
|
+
message: "Invalid Together AI API key. Get one at https://api.together.xyz/settings/api-keys",
|
|
140
|
+
},
|
|
141
|
+
...DEFAULT_ERROR_RULES,
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
providerName: AIProviderName.FIREWORKS,
|
|
146
|
+
aliases: ["fireworks"],
|
|
147
|
+
apiKeyEnvVar: "FIREWORKS_API_KEY",
|
|
148
|
+
baseURLEnvVar: "FIREWORKS_BASE_URL",
|
|
149
|
+
defaultBaseURL: "https://api.fireworks.ai/inference/v1",
|
|
150
|
+
configOptions: createFireworksConfig(),
|
|
151
|
+
modelEnvVar: "FIREWORKS_MODEL",
|
|
152
|
+
defaultModel: FireworksModels.DEEPSEEK_V4_PRO,
|
|
153
|
+
registryDefaultModel: FireworksModels.DEEPSEEK_V4_PRO,
|
|
154
|
+
registryDefaultModelChecksEnvVar: true,
|
|
155
|
+
fallbackModelName: FireworksModels.DEEPSEEK_V4_PRO,
|
|
156
|
+
fallbackModels: [
|
|
157
|
+
FireworksModels.DEEPSEEK_V4_PRO,
|
|
158
|
+
FireworksModels.GLM_5P1,
|
|
159
|
+
FireworksModels.GLM_5,
|
|
160
|
+
FireworksModels.KIMI_K2P6,
|
|
161
|
+
FireworksModels.KIMI_K2P5,
|
|
162
|
+
FireworksModels.GPT_OSS_120B,
|
|
163
|
+
],
|
|
164
|
+
errorRules: [
|
|
165
|
+
{
|
|
166
|
+
match: (ctx) => ctx.statusCode === 401 ||
|
|
167
|
+
/Invalid API key|Authentication/i.test(ctx.message),
|
|
168
|
+
errorClass: AuthenticationError,
|
|
169
|
+
message: "Invalid Fireworks API key. Get one at https://fireworks.ai/account/api-keys",
|
|
170
|
+
},
|
|
171
|
+
...DEFAULT_ERROR_RULES,
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
providerName: AIProviderName.PERPLEXITY,
|
|
176
|
+
aliases: ["perplexity", "pplx"],
|
|
177
|
+
apiKeyEnvVar: "PERPLEXITY_API_KEY",
|
|
178
|
+
baseURLEnvVar: "PERPLEXITY_BASE_URL",
|
|
179
|
+
defaultBaseURL: "https://api.perplexity.ai",
|
|
180
|
+
configOptions: createPerplexityConfig(),
|
|
181
|
+
modelEnvVar: "PERPLEXITY_MODEL",
|
|
182
|
+
defaultModel: PerplexityModels.SONAR,
|
|
183
|
+
registryDefaultModel: PerplexityModels.SONAR,
|
|
184
|
+
registryDefaultModelChecksEnvVar: true,
|
|
185
|
+
// Perplexity's live class does NOT override getFallbackModelName() — it
|
|
186
|
+
// inherits the base class default "gpt-3.5-turbo". Preserved here
|
|
187
|
+
// verbatim, not "fixed" to a Perplexity model — that's a real,
|
|
188
|
+
// pre-existing quirk this plan is not authorized to change.
|
|
189
|
+
fallbackModelName: "gpt-3.5-turbo",
|
|
190
|
+
fallbackModels: [
|
|
191
|
+
PerplexityModels.SONAR,
|
|
192
|
+
PerplexityModels.SONAR_PRO,
|
|
193
|
+
PerplexityModels.SONAR_REASONING,
|
|
194
|
+
PerplexityModels.SONAR_REASONING_PRO,
|
|
195
|
+
PerplexityModels.SONAR_DEEP_RESEARCH,
|
|
196
|
+
],
|
|
197
|
+
errorRules: [
|
|
198
|
+
{
|
|
199
|
+
match: (ctx) => ctx.statusCode === 401 ||
|
|
200
|
+
/Invalid API key|Authentication/i.test(ctx.message),
|
|
201
|
+
errorClass: AuthenticationError,
|
|
202
|
+
message: "Invalid Perplexity API key. Get one at https://www.perplexity.ai/settings/api",
|
|
203
|
+
},
|
|
204
|
+
...DEFAULT_ERROR_RULES,
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
providerName: AIProviderName.MISTRAL,
|
|
209
|
+
aliases: ["mistral"],
|
|
210
|
+
apiKeyEnvVar: "MISTRAL_API_KEY",
|
|
211
|
+
baseURLEnvVar: "MISTRAL_BASE_URL",
|
|
212
|
+
defaultBaseURL: "https://api.mistral.ai/v1",
|
|
213
|
+
configOptions: createMistralConfig(),
|
|
214
|
+
modelEnvVar: "MISTRAL_MODEL",
|
|
215
|
+
defaultModel: MistralModels.MISTRAL_SMALL_2506,
|
|
216
|
+
// The one documented registry-vs-class default-model quirk (see this
|
|
217
|
+
// plan's "Design reference" section): the registry passes the bare
|
|
218
|
+
// literal MISTRAL_LARGE_LATEST with no env-var check, while
|
|
219
|
+
// MistralProvider.getDefaultModel() checks MISTRAL_MODEL and defaults to
|
|
220
|
+
// MISTRAL_SMALL_2506. Preserved exactly, not reconciled.
|
|
221
|
+
registryDefaultModel: MistralModels.MISTRAL_LARGE_LATEST,
|
|
222
|
+
registryDefaultModelChecksEnvVar: false,
|
|
223
|
+
fallbackModelName: MistralModels.MISTRAL_SMALL_2506,
|
|
224
|
+
fallbackModels: [
|
|
225
|
+
MistralModels.MISTRAL_SMALL_2506,
|
|
226
|
+
MistralModels.MISTRAL_LARGE_LATEST,
|
|
227
|
+
],
|
|
228
|
+
errorRules: [
|
|
229
|
+
{
|
|
230
|
+
match: (ctx) => ctx.statusCode === 401 ||
|
|
231
|
+
/API_KEY_INVALID|Invalid API key|Unauthorized/i.test(ctx.message),
|
|
232
|
+
errorClass: AuthenticationError,
|
|
233
|
+
message: "Invalid Mistral API key. Please check your MISTRAL_API_KEY environment variable.",
|
|
234
|
+
},
|
|
235
|
+
...DEFAULT_ERROR_RULES,
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
providerName: AIProviderName.CLOUDFLARE,
|
|
240
|
+
aliases: ["cloudflare", "workers-ai", "cf-ai"],
|
|
241
|
+
apiKeyEnvVar: "CLOUDFLARE_API_KEY",
|
|
242
|
+
computedBaseURL: {
|
|
243
|
+
envVar: "CLOUDFLARE_ACCOUNT_ID",
|
|
244
|
+
missingValueMessage: "CLOUDFLARE_ACCOUNT_ID is required (or pass credentials.cloudflare.accountId). Get the account id from https://dash.cloudflare.com/",
|
|
245
|
+
build: buildCloudflareBaseURL,
|
|
246
|
+
},
|
|
247
|
+
configOptions: createCloudflareConfig(),
|
|
248
|
+
modelEnvVar: "CLOUDFLARE_MODEL",
|
|
249
|
+
defaultModel: CloudflareModels.LLAMA_3_3_70B_FAST,
|
|
250
|
+
registryDefaultModel: CloudflareModels.LLAMA_3_3_70B_FAST,
|
|
251
|
+
registryDefaultModelChecksEnvVar: true,
|
|
252
|
+
fallbackModelName: CloudflareModels.LLAMA_3_1_8B_FAST,
|
|
253
|
+
fallbackModels: [
|
|
254
|
+
CloudflareModels.LLAMA_3_3_70B_FAST,
|
|
255
|
+
CloudflareModels.LLAMA_3_1_70B_INSTRUCT,
|
|
256
|
+
CloudflareModels.LLAMA_3_1_8B_FAST,
|
|
257
|
+
CloudflareModels.LLAMA_3_2_11B_VISION,
|
|
258
|
+
CloudflareModels.MISTRAL_7B_INSTRUCT_V0_2,
|
|
259
|
+
CloudflareModels.QWEN_1P5_14B_CHAT_AWQ,
|
|
260
|
+
],
|
|
261
|
+
errorRules: [
|
|
262
|
+
{
|
|
263
|
+
match: (ctx) => ctx.statusCode === 401 ||
|
|
264
|
+
/Invalid API key|Authentication/i.test(ctx.message),
|
|
265
|
+
errorClass: AuthenticationError,
|
|
266
|
+
message: "Invalid Cloudflare API key. Use a token with Workers AI Read+Write scope. Get one at https://dash.cloudflare.com/profile/api-tokens",
|
|
267
|
+
},
|
|
268
|
+
...DEFAULT_ERROR_RULES,
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
//# sourceMappingURL=openaiCompatCatalog.js.map
|
|
@@ -8,6 +8,7 @@ import { SpanStatusCode, propagation, context } from "@opentelemetry/api";
|
|
|
8
8
|
import { tracers } from "../telemetry/tracers.js";
|
|
9
9
|
import { shouldBypassProxy } from "./utils/noProxyUtils.js";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
+
import { TRANSIENT_NETWORK_CODES } from "../constants/networkErrorCodes.js";
|
|
11
12
|
async function getLangfuseContext() {
|
|
12
13
|
try {
|
|
13
14
|
// Dynamic import to avoid hard dependency — getLangfuseContext is only
|
|
@@ -79,15 +80,6 @@ function extractHostname(url) {
|
|
|
79
80
|
return "[unknown]";
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
/** Error codes classified as transient (module-scope: the retry path is hot). */
|
|
83
|
-
const TRANSIENT_NETWORK_CODES = new Set([
|
|
84
|
-
"ECONNRESET",
|
|
85
|
-
"ETIMEDOUT",
|
|
86
|
-
"ECONNREFUSED",
|
|
87
|
-
"EPIPE",
|
|
88
|
-
"UND_ERR_SOCKET",
|
|
89
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
90
|
-
]);
|
|
91
83
|
/**
|
|
92
84
|
* Classify a fetch failure as a transient network error worth retrying.
|
|
93
85
|
*
|
package/dist/lib/types/cli.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ValidationSchema } from "./aliases.js";
|
|
|
8
8
|
import type { EnhancedGenerateResult, GenerateResult, TextGenerationOptions } from "./generate.js";
|
|
9
9
|
import type { MultimodalAudioEntry } from "./file.js";
|
|
10
10
|
import type { StreamOptions, StreamResult } from "./stream.js";
|
|
11
|
+
import type { ProviderErrorRule } from "./errors.js";
|
|
11
12
|
import type { ExternalMCPToolInfo } from "./externalMcp.js";
|
|
12
13
|
import type { ClaudeSubscriptionTier, AnthropicAuthMethod, AnthropicAuthConfig, SubscriptionInfo, OAuthToken } from "./subscription.js";
|
|
13
14
|
import type { Tool } from "./tools.js";
|
|
@@ -577,6 +578,104 @@ export type ProviderConfigOptions = {
|
|
|
577
578
|
fallbackEnvVars?: string[];
|
|
578
579
|
optional?: boolean;
|
|
579
580
|
};
|
|
581
|
+
/**
|
|
582
|
+
* Minimal credential shape accepted by resolveOpenAICompatConfig() and
|
|
583
|
+
* ConfiguredOpenAICompatProvider. A structural superset of every real
|
|
584
|
+
* per-provider NeurolinkCredentials["<key>"] slice in this family (groq,
|
|
585
|
+
* xai, together, fireworks, perplexity, mistral, cloudflare) — all fields
|
|
586
|
+
* optional, so passing e.g. NeurolinkCredentials["groq"] (which has no
|
|
587
|
+
* accountId) here is always structurally valid.
|
|
588
|
+
*/
|
|
589
|
+
export type OpenAICompatCredentials = {
|
|
590
|
+
apiKey?: string;
|
|
591
|
+
baseURL?: string;
|
|
592
|
+
accountId?: string;
|
|
593
|
+
};
|
|
594
|
+
/**
|
|
595
|
+
* One row of the config-driven OpenAI-compatible provider catalog
|
|
596
|
+
* (OPENAI_COMPAT_CATALOG, src/lib/providers/openaiCompatCatalog.ts).
|
|
597
|
+
* Replaces a hand-written OpenAIChatCompletionsProvider subclass for
|
|
598
|
+
* providers whose only differences from every sibling are credentials,
|
|
599
|
+
* base URL, model defaults, and error-message classification.
|
|
600
|
+
*/
|
|
601
|
+
export type OpenAICompatCatalogEntry = {
|
|
602
|
+
/** Registry key / nl.generate({provider}) value, e.g. "groq". */
|
|
603
|
+
providerName: AIProviderName;
|
|
604
|
+
/** Registry aliases, e.g. ["together-ai", "together"]. */
|
|
605
|
+
aliases: string[];
|
|
606
|
+
/**
|
|
607
|
+
* Env var holding the API key, e.g. "GROQ_API_KEY".
|
|
608
|
+
*
|
|
609
|
+
* Declarative: the key is actually read through `configOptions.envVarName`,
|
|
610
|
+
* which `validateApiKey` consults. This field exists so an entry states its
|
|
611
|
+
* credential source without a caller having to reach into configOptions,
|
|
612
|
+
* and the catalog suite asserts the two always name the same variable — two
|
|
613
|
+
* fields describing one fact are worth nothing if they can disagree.
|
|
614
|
+
*/
|
|
615
|
+
apiKeyEnvVar: string;
|
|
616
|
+
/**
|
|
617
|
+
* Env var that can override the base URL, e.g. "GROQ_BASE_URL". Omit
|
|
618
|
+
* for entries that use computedBaseURL instead (e.g. Cloudflare).
|
|
619
|
+
*/
|
|
620
|
+
baseURLEnvVar?: string;
|
|
621
|
+
/** Static default base URL. Omit for computedBaseURL entries. */
|
|
622
|
+
defaultBaseURL?: string;
|
|
623
|
+
/**
|
|
624
|
+
* Present only for providers whose base URL is computed from an extra
|
|
625
|
+
* required credential value instead of a static default (Cloudflare's
|
|
626
|
+
* accountId). Deliberately narrow (accountId-shaped) rather than a
|
|
627
|
+
* generic extra-field mechanism — Cloudflare is the only current user.
|
|
628
|
+
*/
|
|
629
|
+
computedBaseURL?: {
|
|
630
|
+
/** Env var fallback for the extra value, e.g. "CLOUDFLARE_ACCOUNT_ID". */
|
|
631
|
+
envVar: string;
|
|
632
|
+
/** Thrown when neither credentials.accountId nor envVar supply a value. */
|
|
633
|
+
missingValueMessage: string;
|
|
634
|
+
/** Builds the base URL from the resolved accountId. */
|
|
635
|
+
build: (accountId: string) => string;
|
|
636
|
+
};
|
|
637
|
+
/** Setup/help metadata, passed to validateApiKey(). Not consumed by
|
|
638
|
+
* classifyProviderError() — that function's ProviderErrorContext has no
|
|
639
|
+
* docsUrl field; any URL a rule's message needs is inlined in the rule
|
|
640
|
+
* itself (see Task 4). */
|
|
641
|
+
configOptions: ProviderConfigOptions;
|
|
642
|
+
/** Env var for the default model, e.g. "GROQ_MODEL". */
|
|
643
|
+
modelEnvVar: string;
|
|
644
|
+
/** Default model when modelEnvVar is unset. */
|
|
645
|
+
defaultModel: string;
|
|
646
|
+
/**
|
|
647
|
+
* The literal passed as ProviderFactory.registerProvider()'s defaultModel
|
|
648
|
+
* argument (resolved before the provider is constructed). Preserves each
|
|
649
|
+
* provider's exact pre-migration registry behavior.
|
|
650
|
+
*/
|
|
651
|
+
registryDefaultModel: string;
|
|
652
|
+
/**
|
|
653
|
+
* True for every provider except Mistral: whether the registry-level
|
|
654
|
+
* default also consults modelEnvVar before falling back to
|
|
655
|
+
* registryDefaultModel. False is a pre-existing, intentionally-preserved
|
|
656
|
+
* quirk unique to Mistral's registration (see plan's Design reference).
|
|
657
|
+
*/
|
|
658
|
+
registryDefaultModelChecksEnvVar: boolean;
|
|
659
|
+
/** Fallback model name (getFallbackModelName()). */
|
|
660
|
+
fallbackModelName: string;
|
|
661
|
+
/** Fallback model list (getFallbackModels()). */
|
|
662
|
+
fallbackModels: string[];
|
|
663
|
+
/**
|
|
664
|
+
* Error-classification rules, consumed by classifyProviderError. Typed
|
|
665
|
+
* as a mutable array — not readonly — because plan 07's
|
|
666
|
+
* `classifyProviderError(error, rules: ProviderErrorRule[], provider, modelName?)`
|
|
667
|
+
* declares `rules` as `ProviderErrorRule[]`; a `readonly` array here
|
|
668
|
+
* would not be assignable to that parameter without a cast, which rule
|
|
669
|
+
* 14 (no double assertions) and general hygiene both rule out. Each
|
|
670
|
+
* entry's array is still constructed as a fresh literal per provider in
|
|
671
|
+
* Task 4, so nothing actually mutates it at runtime.
|
|
672
|
+
*/
|
|
673
|
+
errorRules: ProviderErrorRule[];
|
|
674
|
+
};
|
|
675
|
+
/** The subset of OpenAICompatCatalogEntry that resolveOpenAICompatConfig()
|
|
676
|
+
* needs — lets call sites pass a minimal object without the full catalog
|
|
677
|
+
* entry (e.g. in tests, or a future non-catalog caller). */
|
|
678
|
+
export type OpenAICompatConfigInput = Pick<OpenAICompatCatalogEntry, "providerName" | "apiKeyEnvVar" | "baseURLEnvVar" | "defaultBaseURL" | "computedBaseURL" | "configOptions">;
|
|
580
679
|
/**
|
|
581
680
|
* AI Provider type with flexible parameter support
|
|
582
681
|
*/
|
|
@@ -1714,8 +1813,8 @@ export type ProviderDescriptor = {
|
|
|
1714
1813
|
modelFallbacks?: readonly string[];
|
|
1715
1814
|
/** Additional env vars required alongside apiKey (e.g. AWS secret key, Azure endpoint). */
|
|
1716
1815
|
extraRequired?: readonly string[];
|
|
1717
|
-
/** Alternate ways to satisfy extraRequired when it isn't a plain env-var list (e.g. Vertex's file-path-OR-individual-fields auth). */
|
|
1718
|
-
extraRequiredFallbacks?: readonly string[];
|
|
1816
|
+
/** Alternate ways to satisfy extraRequired when it isn't a plain env-var list (e.g. Vertex's file-path-OR-individual-fields auth). Each entry is either a single env var name (satisfied alone) or a nested array of names that must ALL be present together (e.g. Vertex's GOOGLE_AUTH_CLIENT_EMAIL + GOOGLE_AUTH_PRIVATE_KEY pair, which is only valid as a pair). Evaluate with `satisfiesFallbacks()` (providerConfig.ts) rather than re-deriving this logic at each call site. */
|
|
1817
|
+
extraRequiredFallbacks?: readonly (string | readonly string[])[];
|
|
1719
1818
|
/** True when the provider is usable with zero configuration (local runtime with a documented default URL, or a documented non-secret default like LiteLLM's "sk-anything"). */
|
|
1720
1819
|
optional?: boolean;
|
|
1721
1820
|
};
|
|
@@ -1741,6 +1840,21 @@ export type ProviderDescriptor = {
|
|
|
1741
1840
|
autoSelectPriority?: number;
|
|
1742
1841
|
/** Format-validation regex sourced from providerConfig.ts's API_KEY_FORMATS, when one exists for this provider. */
|
|
1743
1842
|
apiKeyFormatPattern?: RegExp;
|
|
1843
|
+
/**
|
|
1844
|
+
* True when this provider's credentials are resolved by an external chain
|
|
1845
|
+
* or its own config validator rather than by plain env-var presence, so
|
|
1846
|
+
* its required-env-vars can't be expressed as "every one of these exact
|
|
1847
|
+
* names must be literally set". Examples: Vertex accepts a service-account
|
|
1848
|
+
* file OR individual client-email/private-key fields OR a base64 key
|
|
1849
|
+
* (an OR, not an AND, of auth paths); Bedrock falls back to the AWS SDK's
|
|
1850
|
+
* own default credential chain (shared profile, IAM role) with no env
|
|
1851
|
+
* vars required at all; LiteLLM is a documented zero-config local proxy.
|
|
1852
|
+
* `ProviderHealthChecker.getRequiredEnvironmentVariables()` returns `[]`
|
|
1853
|
+
* for these providers and defers to `checkProviderSpecificConfig()`'s
|
|
1854
|
+
* dedicated per-provider check instead of deriving a flat AND-list from
|
|
1855
|
+
* `envVars`.
|
|
1856
|
+
*/
|
|
1857
|
+
credentialsResolvedExternally?: boolean;
|
|
1744
1858
|
};
|
|
1745
1859
|
/** Minimal NeuroLink-like instance accepted by the image generation service. */
|
|
1746
1860
|
export type NeuroLinkInstance = {
|
|
@@ -13,21 +13,84 @@
|
|
|
13
13
|
import { ProviderError, AuthenticationError, RateLimitError, InvalidModelError, NetworkError, } from "../types/index.js";
|
|
14
14
|
import { TimeoutError } from "./timeout.js";
|
|
15
15
|
import { duckTypedStatusCode } from "./providerRetry.js";
|
|
16
|
+
import { TRANSIENT_NETWORK_CODES } from "../constants/networkErrorCodes.js";
|
|
17
|
+
import { redactUrlsInText } from "./logSanitize.js";
|
|
18
|
+
/** Bounded walk depth for `.cause` chains — matches the precedent in
|
|
19
|
+
* `proxy/proxyFetch.ts`'s `isTransientNetworkError`. Guards against
|
|
20
|
+
* pathological/cyclic `.cause` chains hanging classification. */
|
|
21
|
+
const MAX_CAUSE_DEPTH = 5;
|
|
22
|
+
/**
|
|
23
|
+
* Walk `error.cause` up to `MAX_CAUSE_DEPTH` links, guarded by a seen-set so
|
|
24
|
+
* a cyclic chain (`a.cause === a`, or a longer cycle) terminates instead of
|
|
25
|
+
* looping. Node's native `fetch` (undici) throws `TypeError: fetch failed`
|
|
26
|
+
* with the real transport error nested under `.cause` — sometimes another
|
|
27
|
+
* level deep (e.g. a SocketError inside a ConnectTimeoutError) — so a
|
|
28
|
+
* classifier that only reads the outer error's `.message`/`.code` never
|
|
29
|
+
* sees it.
|
|
30
|
+
*/
|
|
31
|
+
function collectCauseChain(error) {
|
|
32
|
+
const chain = [];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
let current = error;
|
|
35
|
+
while (current &&
|
|
36
|
+
typeof current === "object" &&
|
|
37
|
+
!seen.has(current) &&
|
|
38
|
+
chain.length < MAX_CAUSE_DEPTH) {
|
|
39
|
+
seen.add(current);
|
|
40
|
+
const record = current;
|
|
41
|
+
chain.push(record);
|
|
42
|
+
current = record.cause;
|
|
43
|
+
}
|
|
44
|
+
return chain;
|
|
45
|
+
}
|
|
46
|
+
function firstString(chain, key) {
|
|
47
|
+
for (const record of chain) {
|
|
48
|
+
if (typeof record[key] === "string") {
|
|
49
|
+
return record[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
16
54
|
function buildErrorContext(error, provider, modelName) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
? record.message
|
|
55
|
+
const chain = collectCauseChain(error);
|
|
56
|
+
const top = chain[0];
|
|
57
|
+
const topMessage = typeof top?.message === "string"
|
|
58
|
+
? top.message
|
|
22
59
|
: error instanceof Error
|
|
23
60
|
? error.message
|
|
24
61
|
: "Unknown error";
|
|
62
|
+
// Compose (never replace) the message: append the deepest cause's message
|
|
63
|
+
// when it differs from the top, so existing rules matching the outer text
|
|
64
|
+
// (e.g. "rate limit", "model not found") keep matching, while the real
|
|
65
|
+
// transport failure buried in .cause becomes visible to rules that need
|
|
66
|
+
// it (e.g. a nested "ECONNREFUSED").
|
|
67
|
+
// The nested message is redacted before it is composed in: an undici cause
|
|
68
|
+
// carries the full request URL, so a presigned token would otherwise reach
|
|
69
|
+
// a client-facing error message through this path. Only the nested text is
|
|
70
|
+
// scrubbed — the provider's own top-level message is left alone, since
|
|
71
|
+
// several providers deliberately name their base URL in it.
|
|
72
|
+
const deepest = chain[chain.length - 1];
|
|
73
|
+
const deepestMessage = typeof deepest?.message === "string"
|
|
74
|
+
? redactUrlsInText(deepest.message)
|
|
75
|
+
: undefined;
|
|
76
|
+
const message = deepestMessage && deepestMessage !== topMessage
|
|
77
|
+
? `${topMessage}: ${deepestMessage}`
|
|
78
|
+
: topMessage;
|
|
79
|
+
// errorCode/errorName/statusCode: prefer the outer error's own value,
|
|
80
|
+
// falling back to the first cause in the chain that has one.
|
|
81
|
+
let statusCode;
|
|
82
|
+
for (const record of chain) {
|
|
83
|
+
statusCode = duckTypedStatusCode(record);
|
|
84
|
+
if (statusCode !== undefined) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
25
88
|
return {
|
|
26
89
|
error,
|
|
27
90
|
message,
|
|
28
|
-
statusCode
|
|
29
|
-
errorName:
|
|
30
|
-
errorCode:
|
|
91
|
+
statusCode,
|
|
92
|
+
errorName: firstString(chain, "name"),
|
|
93
|
+
errorCode: firstString(chain, "code"),
|
|
31
94
|
provider,
|
|
32
95
|
modelName,
|
|
33
96
|
};
|
|
@@ -80,13 +143,39 @@ export const DEFAULT_ERROR_RULES = [
|
|
|
80
143
|
: `${ctx.provider} model not found.`,
|
|
81
144
|
},
|
|
82
145
|
{
|
|
83
|
-
|
|
146
|
+
// Message regex covers providers/SDKs that surface a code as text
|
|
147
|
+
// (e.g. AWS SDK wrapping "ECONNRESET" into its own message). errorCode
|
|
148
|
+
// covers undici's native fetch(), which wraps transport failures as
|
|
149
|
+
// `TypeError: fetch failed` and puts the *structured* code
|
|
150
|
+
// (ECONNREFUSED, UND_ERR_SOCKET, ...) on a nested `.cause` rather than
|
|
151
|
+
// in any message text — buildErrorContext's cause walk surfaces it here.
|
|
152
|
+
match: (ctx) => /ECONNRESET|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|network|connection/i.test(ctx.message) ||
|
|
153
|
+
(ctx.errorCode !== undefined &&
|
|
154
|
+
TRANSIENT_NETWORK_CODES.has(ctx.errorCode)),
|
|
84
155
|
errorClass: NetworkError,
|
|
85
156
|
message: (ctx) => `Connection error: ${ctx.message}`,
|
|
86
157
|
},
|
|
87
158
|
{
|
|
88
|
-
|
|
89
|
-
|
|
159
|
+
// Batch J Task 3: the old `/\b5\d\d\b/` matched ANY bare 3-digit number
|
|
160
|
+
// in [500,599) anywhere in the message — e.g. "max_tokens (500) exceeds
|
|
161
|
+
// model limit" — with no relation to an actual HTTP status. Tightened to
|
|
162
|
+
// require the number sit in a status-shaped context: immediately next
|
|
163
|
+
// to "error" (either order) or "status"/"status code" (a common HTTP
|
|
164
|
+
// client wrapper phrase, e.g. axios's "Request failed with status code
|
|
165
|
+
// 500"), with a bounded gap so unrelated digits nearby can't bridge the
|
|
166
|
+
// match — or a named 5xx phrase that needs no digit at all ("bad
|
|
167
|
+
// gateway", "service unavailable", "gateway timeout", "server error",
|
|
168
|
+
// which already covers "... Internal Server Error"). This changes the
|
|
169
|
+
// MATCHED MESSAGE TEXT only, never the classified class: when no rule
|
|
170
|
+
// matches, `classifyProviderError`'s fallback also returns
|
|
171
|
+
// `ProviderError` (see above) — the same class this rule assigns — so
|
|
172
|
+
// narrowing this regex can only move a message between "${provider}
|
|
173
|
+
// server error: ..." and "${provider} error: ...", never between error
|
|
174
|
+
// classes.
|
|
175
|
+
match: (ctx) => (ctx.statusCode !== undefined &&
|
|
176
|
+
ctx.statusCode >= 500 &&
|
|
177
|
+
ctx.statusCode <= 599) ||
|
|
178
|
+
/server error|bad gateway|service unavailable|gateway timeout|\berror\b\D{0,12}\b5\d\d\b|\b5\d\d\b\D{0,12}\berror\b|\bstatus(?:\s*code)?\b\D{0,12}\b5\d\d\b/i.test(ctx.message),
|
|
90
179
|
errorClass: ProviderError,
|
|
91
180
|
message: (ctx) => `${ctx.provider} server error: ${ctx.message}`,
|
|
92
181
|
},
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Enhanced with format validation and advanced error classification
|
|
6
6
|
* Extended with Claude subscription OAuth support
|
|
7
7
|
*/
|
|
8
|
-
import type { APIValidationResult, ProviderConfigOptions, AnthropicAuthMethod, ClaudeSubscriptionTier, AnthropicAuthConfig, AnthropicAuthConfigResult } from "../types/index.js";
|
|
8
|
+
import type { APIValidationResult, ProviderConfigOptions, AnthropicAuthMethod, ClaudeSubscriptionTier, AnthropicAuthConfig, AnthropicAuthConfigResult, OpenAICompatConfigInput, OpenAICompatCredentials } from "../types/index.js";
|
|
9
9
|
/**
|
|
10
10
|
* API key format validation patterns (extracted from advanced validation system)
|
|
11
11
|
* Exported for use across the codebase to replace scattered regex patterns
|
|
@@ -70,6 +70,22 @@ export declare function getProviderModel(envVar: string, defaultModel: string):
|
|
|
70
70
|
* @returns True if one of the credentials is available
|
|
71
71
|
*/
|
|
72
72
|
export declare function hasProviderCredentials(envVars: string[]): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Evaluates a `ProviderDescriptor.envVars.extraRequiredFallbacks`-shaped
|
|
75
|
+
* list against an env-var source. Each entry is either a single env var
|
|
76
|
+
* name (satisfied on its own) or a nested array of names that must ALL be
|
|
77
|
+
* present together (e.g. Vertex's GOOGLE_AUTH_CLIENT_EMAIL +
|
|
78
|
+
* GOOGLE_AUTH_PRIVATE_KEY pair, which is only valid auth as a pair).
|
|
79
|
+
* Returns true when at least one entry is satisfied. The single evaluation
|
|
80
|
+
* site for this shape — every consumer (providerUtils.ts, providerHealth.ts,
|
|
81
|
+
* setup.ts, environmentManager.ts) must call this instead of re-deriving the
|
|
82
|
+
* same `.some()`/`.every()` logic, so they can't drift out of sync with each
|
|
83
|
+
* other or with the real auth gate (hasGoogleCredentials()).
|
|
84
|
+
* @param env Explicit env-var source (`process.env`, or a parsed .env file) —
|
|
85
|
+
* never hardcoded, so callers checking a file's contents (not the live
|
|
86
|
+
* process env) can reuse this too.
|
|
87
|
+
*/
|
|
88
|
+
export declare function satisfiesFallbacks(fallbacks: readonly (string | readonly string[])[] | undefined, env: Record<string, string | undefined>): boolean;
|
|
73
89
|
/**
|
|
74
90
|
* Creates Anthropic provider configuration
|
|
75
91
|
* Supports both API key and OAuth authentication methods
|
|
@@ -353,3 +369,25 @@ export declare function hasSubscriptionFeature(feature: "extended_thinking" | "p
|
|
|
353
369
|
* @returns Human-readable configuration description
|
|
354
370
|
*/
|
|
355
371
|
export declare function describeAnthropicConfig(): string;
|
|
372
|
+
/**
|
|
373
|
+
* Resolves the {apiKey, baseURL} pair for a config-driven OpenAI-compatible
|
|
374
|
+
* catalog entry (see OpenAICompatCatalogEntry in types/providers.ts).
|
|
375
|
+
*
|
|
376
|
+
* Extracted from the identical 6-line precedence block that was copy-pasted
|
|
377
|
+
* across groq.ts, xai.ts, togetherAi.ts, fireworks.ts, perplexity.ts, and
|
|
378
|
+
* mistral.ts, plus Cloudflare's accountId-computed-baseURL variant.
|
|
379
|
+
*
|
|
380
|
+
* Precedence (matches every ported subclass's original behavior exactly):
|
|
381
|
+
* apiKey: credentials.apiKey (trimmed, non-blank) > env var > throw
|
|
382
|
+
* baseURL: credentials.baseURL (trimmed, non-blank)
|
|
383
|
+
* > env var (if entry.baseURLEnvVar is set, trimmed, non-blank)
|
|
384
|
+
* > entry.defaultBaseURL
|
|
385
|
+
* baseURL (computedBaseURL entries, e.g. Cloudflare):
|
|
386
|
+
* credentials.baseURL > computedBaseURL.build(accountId), where
|
|
387
|
+
* accountId = credentials.accountId (trimmed) > env var (trimmed)
|
|
388
|
+
* > throw computedBaseURL.missingValueMessage
|
|
389
|
+
*/
|
|
390
|
+
export declare function resolveOpenAICompatConfig(entry: OpenAICompatConfigInput, credentials?: OpenAICompatCredentials): {
|
|
391
|
+
apiKey: string;
|
|
392
|
+
baseURL: string;
|
|
393
|
+
};
|