@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/browser/neurolink.min.js +367 -367
  3. package/dist/cli/commands/setup.d.ts +4 -1
  4. package/dist/cli/commands/setup.js +44 -14
  5. package/dist/constants/networkErrorCodes.d.ts +14 -0
  6. package/dist/constants/networkErrorCodes.js +21 -0
  7. package/dist/factories/providerDescriptors.js +13 -3
  8. package/dist/lib/constants/networkErrorCodes.d.ts +14 -0
  9. package/dist/lib/constants/networkErrorCodes.js +22 -0
  10. package/dist/lib/factories/providerDescriptors.js +13 -3
  11. package/dist/lib/processors/base/BaseFileProcessor.d.ts +17 -0
  12. package/dist/lib/processors/base/BaseFileProcessor.js +40 -0
  13. package/dist/lib/processors/document/OpenDocumentProcessor.js +12 -2
  14. package/dist/lib/providers/configuredOpenAICompat.d.ts +24 -0
  15. package/dist/lib/providers/configuredOpenAICompat.js +60 -0
  16. package/dist/lib/providers/openaiCompatCatalog.d.ts +24 -0
  17. package/dist/lib/providers/openaiCompatCatalog.js +272 -0
  18. package/dist/lib/proxy/proxyFetch.js +1 -9
  19. package/dist/lib/types/cli.d.ts +2 -0
  20. package/dist/lib/types/providers.d.ts +116 -2
  21. package/dist/lib/utils/errorClassifier.js +100 -11
  22. package/dist/lib/utils/providerConfig.d.ts +39 -1
  23. package/dist/lib/utils/providerConfig.js +83 -0
  24. package/dist/lib/utils/providerHealth.d.ts +30 -24
  25. package/dist/lib/utils/providerHealth.js +42 -41
  26. package/dist/lib/utils/providerUtils.js +2 -2
  27. package/dist/processors/base/BaseFileProcessor.d.ts +17 -0
  28. package/dist/processors/base/BaseFileProcessor.js +40 -0
  29. package/dist/processors/document/OpenDocumentProcessor.js +12 -2
  30. package/dist/providers/configuredOpenAICompat.d.ts +24 -0
  31. package/dist/providers/configuredOpenAICompat.js +59 -0
  32. package/dist/providers/openaiCompatCatalog.d.ts +24 -0
  33. package/dist/providers/openaiCompatCatalog.js +271 -0
  34. package/dist/proxy/proxyFetch.js +1 -9
  35. package/dist/types/cli.d.ts +2 -0
  36. package/dist/types/providers.d.ts +116 -2
  37. package/dist/utils/errorClassifier.js +100 -11
  38. package/dist/utils/providerConfig.d.ts +39 -1
  39. package/dist/utils/providerConfig.js +83 -0
  40. package/dist/utils/providerHealth.d.ts +30 -24
  41. package/dist/utils/providerHealth.js +42 -41
  42. package/dist/utils/providerUtils.js +2 -2
  43. package/package.json +2 -1
@@ -0,0 +1,271 @@
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
+ ];
@@ -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
  *
@@ -583,6 +583,8 @@ export type SetupArgs = {
583
583
  status?: boolean;
584
584
  interactive?: boolean;
585
585
  help?: boolean;
586
+ check?: boolean;
587
+ nonInteractive?: boolean;
586
588
  };
587
589
  /**
588
590
  * Narrowed ProviderInfo used by the main `neurolink setup` command,
@@ -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 record = error && typeof error === "object"
18
- ? error
19
- : undefined;
20
- const message = typeof record?.message === "string"
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: duckTypedStatusCode(error),
29
- errorName: typeof record?.name === "string" ? record.name : undefined,
30
- errorCode: typeof record?.code === "string" ? record.code : undefined,
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
- match: (ctx) => /ECONNRESET|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|network|connection/i.test(ctx.message),
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
- match: (ctx) => (ctx.statusCode !== undefined && ctx.statusCode >= 500) ||
89
- /\b5\d\d\b|server error/i.test(ctx.message),
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
+ };