@juspay/neurolink 11.1.0 → 11.1.1

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 (35) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/browser/neurolink.min.js +390 -390
  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/proxy/proxyFetch.js +1 -9
  15. package/dist/lib/types/cli.d.ts +2 -0
  16. package/dist/lib/types/providers.d.ts +17 -2
  17. package/dist/lib/utils/errorClassifier.js +100 -11
  18. package/dist/lib/utils/providerConfig.d.ts +16 -0
  19. package/dist/lib/utils/providerConfig.js +23 -0
  20. package/dist/lib/utils/providerHealth.d.ts +30 -24
  21. package/dist/lib/utils/providerHealth.js +42 -41
  22. package/dist/lib/utils/providerUtils.js +2 -2
  23. package/dist/processors/base/BaseFileProcessor.d.ts +17 -0
  24. package/dist/processors/base/BaseFileProcessor.js +40 -0
  25. package/dist/processors/document/OpenDocumentProcessor.js +12 -2
  26. package/dist/proxy/proxyFetch.js +1 -9
  27. package/dist/types/cli.d.ts +2 -0
  28. package/dist/types/providers.d.ts +17 -2
  29. package/dist/utils/errorClassifier.js +100 -11
  30. package/dist/utils/providerConfig.d.ts +16 -0
  31. package/dist/utils/providerConfig.js +23 -0
  32. package/dist/utils/providerHealth.d.ts +30 -24
  33. package/dist/utils/providerHealth.js +42 -41
  34. package/dist/utils/providerUtils.js +2 -2
  35. package/package.json +1 -1
@@ -28,4 +28,7 @@ export declare function checkExistingConfigurations(): Promise<string[]>;
28
28
  /**
29
29
  * Delegate to existing provider setup commands
30
30
  */
31
- export declare function delegateToProviderSetup(providerId: string): Promise<void>;
31
+ export declare function delegateToProviderSetup(providerId: string, flags?: {
32
+ check?: boolean;
33
+ nonInteractive?: boolean;
34
+ }): Promise<void>;
@@ -20,7 +20,7 @@ import { handleGCPSetup } from "./setup-gcp.js";
20
20
  import { handleHuggingFaceSetup } from "./setup-huggingface.js";
21
21
  import { handleMistralSetup } from "./setup-mistral.js";
22
22
  import { PROVIDER_DESCRIPTORS_BY_NAME } from "../../lib/factories/providerDescriptors.js";
23
- import { createCloudflareConfig, createCohereConfig, createDeepSeekConfig, createFireworksConfig, createGroqConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createPerplexityConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createTogetherAIConfig, createVoyageConfig, createXaiConfig, } from "../../lib/utils/providerConfig.js";
23
+ import { createCloudflareConfig, createCohereConfig, createDeepSeekConfig, createFireworksConfig, createGroqConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createPerplexityConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createTogetherAIConfig, createVoyageConfig, createXaiConfig, satisfiesFallbacks, } from "../../lib/utils/providerConfig.js";
24
24
  // Provider information database
25
25
  const PROVIDERS = [
26
26
  {
@@ -277,15 +277,22 @@ function printGenericProviderSetup(providerId, config) {
277
277
  */
278
278
  export async function handleSetup(argv) {
279
279
  try {
280
- // Handle specific flags
280
+ // Handle specific flags. `--list`/`--status` are documented as one-shot,
281
+ // non-interactive informational commands (see the `setup --list` /
282
+ // `setup --status` examples in setupCommandFactory.ts) — pass
283
+ // interactive: false so they print and return instead of chaining into
284
+ // the wizard's follow-up inquirer prompt below.
281
285
  if (argv.list) {
282
- return await showProviderList();
286
+ return await showProviderList(false);
283
287
  }
284
288
  if (argv.status) {
285
- return await showProviderStatus();
289
+ return await showProviderStatus(false);
286
290
  }
287
291
  if (argv.provider && argv.provider !== "auto") {
288
- return await delegateToProviderSetup(argv.provider);
292
+ return await delegateToProviderSetup(argv.provider, {
293
+ check: argv.check,
294
+ nonInteractive: argv.nonInteractive,
295
+ });
289
296
  }
290
297
  // Main setup wizard
291
298
  await showWelcomeScreen();
@@ -399,7 +406,7 @@ export async function checkExistingConfigurations() {
399
406
  continue;
400
407
  }
401
408
  const requiredOk = (extraRequired ?? []).every((v) => !!process.env[v]) ||
402
- (extraRequiredFallbacks ?? []).some((v) => !!process.env[v]);
409
+ satisfiesFallbacks(extraRequiredFallbacks, process.env);
403
410
  if (requiredOk) {
404
411
  configured.push(p.id);
405
412
  }
@@ -482,11 +489,16 @@ async function runProviderSelection() {
482
489
  /**
483
490
  * Delegate to existing provider setup commands
484
491
  */
485
- export async function delegateToProviderSetup(providerId) {
492
+ export async function delegateToProviderSetup(providerId, flags = {
493
+ check: false,
494
+ nonInteractive: false,
495
+ }) {
496
+ const nonInteractive = flags.nonInteractive ?? false;
497
+ const check = flags.check ?? false;
486
498
  const setupArgs = {
487
- nonInteractive: false,
488
- "non-interactive": false,
489
- check: false,
499
+ nonInteractive,
500
+ "non-interactive": nonInteractive,
501
+ check,
490
502
  _: [],
491
503
  $0: "neurolink",
492
504
  };
@@ -580,9 +592,15 @@ async function showSetupCompletion(providerId) {
580
592
  logger.always(chalk.blue("Happy generating! 🚀"));
581
593
  }
582
594
  /**
583
- * Show detailed provider information
595
+ * Show detailed provider information.
596
+ *
597
+ * @param interactive - When true (the default, used by the interactive
598
+ * wizard's "learn more" menu choice), follows up with a prompt offering to
599
+ * launch the setup wizard. When false (used by the direct `setup --list`
600
+ * flag), returns immediately after printing so the command exits cleanly
601
+ * without waiting on stdin.
584
602
  */
585
- async function showProviderList() {
603
+ async function showProviderList(interactive = true) {
586
604
  logger.always(chalk.blue("📚 NeuroLink Supported AI Providers"));
587
605
  logger.always("");
588
606
  for (const provider of PROVIDERS) {
@@ -612,6 +630,9 @@ async function showProviderList() {
612
630
  logger.always("• neurolink setup --provider azure");
613
631
  logger.always("• neurolink setup --provider bedrock");
614
632
  logger.always("");
633
+ if (!interactive) {
634
+ return;
635
+ }
615
636
  const { action } = await inquirer.prompt([
616
637
  {
617
638
  type: "select",
@@ -628,9 +649,15 @@ async function showProviderList() {
628
649
  }
629
650
  }
630
651
  /**
631
- * Show provider status with detailed information
652
+ * Show provider status with detailed information.
653
+ *
654
+ * @param interactive - When true (the default, used by the interactive
655
+ * wizard's "check status" menu choice), follows up with a prompt offering to
656
+ * set up another provider. When false (used by the direct `setup --status`
657
+ * flag), returns immediately after printing so the command exits cleanly
658
+ * without waiting on stdin.
632
659
  */
633
- async function showProviderStatus() {
660
+ async function showProviderStatus(interactive = true) {
634
661
  const spinner = ora("🔍 Checking all AI provider configurations...").start();
635
662
  try {
636
663
  // Get provider status using NeuroLink SDK
@@ -691,6 +718,9 @@ async function showProviderStatus() {
691
718
  logger.always("• Check performance: neurolink provider status");
692
719
  logger.always("");
693
720
  }
721
+ if (!interactive) {
722
+ return;
723
+ }
694
724
  const { setupAnother } = await inquirer.prompt([
695
725
  {
696
726
  type: "confirm",
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Error codes that indicate a transient transport failure (dropped
3
+ * connection, socket reset, connect timeout) rather than a permanent
4
+ * misconfiguration. Shared between `proxy/proxyFetch.ts` (retry gating) and
5
+ * `utils/errorClassifier.ts` (NetworkError classification) so both stay in
6
+ * sync — a second hand-maintained copy would drift the two apart the same
7
+ * way the "5xx literal text vs statusCode" split did.
8
+ *
9
+ * undici's native `fetch()` wraps the real transport failure in
10
+ * `TypeError: fetch failed`, with the actionable code on `error.cause`
11
+ * (sometimes nested another level deep, e.g. a SocketError inside a
12
+ * ConnectTimeoutError) — never on the outer TypeError itself.
13
+ */
14
+ export declare const TRANSIENT_NETWORK_CODES: ReadonlySet<string>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Error codes that indicate a transient transport failure (dropped
3
+ * connection, socket reset, connect timeout) rather than a permanent
4
+ * misconfiguration. Shared between `proxy/proxyFetch.ts` (retry gating) and
5
+ * `utils/errorClassifier.ts` (NetworkError classification) so both stay in
6
+ * sync — a second hand-maintained copy would drift the two apart the same
7
+ * way the "5xx literal text vs statusCode" split did.
8
+ *
9
+ * undici's native `fetch()` wraps the real transport failure in
10
+ * `TypeError: fetch failed`, with the actionable code on `error.cause`
11
+ * (sometimes nested another level deep, e.g. a SocketError inside a
12
+ * ConnectTimeoutError) — never on the outer TypeError itself.
13
+ */
14
+ export const TRANSIENT_NETWORK_CODES = new Set([
15
+ "ECONNRESET",
16
+ "ETIMEDOUT",
17
+ "ECONNREFUSED",
18
+ "EPIPE",
19
+ "UND_ERR_SOCKET",
20
+ "UND_ERR_CONNECT_TIMEOUT",
21
+ ]);
@@ -26,6 +26,9 @@ export const PROVIDER_DESCRIPTORS = [
26
26
  timeouts: { generateMs: 45_000, streamMs: 120_000 },
27
27
  autoSelectPriority: 7,
28
28
  apiKeyFormatPattern: API_KEY_FORMATS.bedrock,
29
+ // Falls back to the AWS SDK's own default credential chain (shared
30
+ // profile, IAM role) when these env vars are absent — see field JSDoc.
31
+ credentialsResolvedExternally: true,
29
32
  },
30
33
  {
31
34
  name: AIProviderName.OPENAI,
@@ -89,12 +92,14 @@ export const PROVIDER_DESCRIPTORS = [
89
92
  // priority than GOOGLE_APPLICATION_CREDENTIALS by the real gating
90
93
  // logic (hasGoogleCredentials() in googleVertex/client.ts and
91
94
  // googleVertex/utils.ts) — corrected here vs. the plan snippet, which
92
- // omitted it.
95
+ // omitted it. GOOGLE_AUTH_CLIENT_EMAIL and GOOGLE_AUTH_PRIVATE_KEY are
96
+ // nested together because hasGoogleCredentials() only accepts them as
97
+ // a pair — either alone is not valid auth, unlike the other flat
98
+ // entries here which are each independently sufficient.
93
99
  extraRequiredFallbacks: [
94
100
  "GOOGLE_APPLICATION_CREDENTIALS_NEUROLINK",
95
101
  "GOOGLE_SERVICE_ACCOUNT_KEY",
96
- "GOOGLE_AUTH_CLIENT_EMAIL",
97
- "GOOGLE_AUTH_PRIVATE_KEY",
102
+ ["GOOGLE_AUTH_CLIENT_EMAIL", "GOOGLE_AUTH_PRIVATE_KEY"],
98
103
  ],
99
104
  },
100
105
  defaultModel: VertexModels.CLAUDE_4_6_SONNET,
@@ -104,6 +109,9 @@ export const PROVIDER_DESCRIPTORS = [
104
109
  setupUrl: "https://console.cloud.google.com/",
105
110
  timeouts: { generateMs: 60_000, streamMs: 120_000 },
106
111
  autoSelectPriority: 3,
112
+ // OR-of-multiple-auth-paths (file / individual fields / base64 key) —
113
+ // not a flat AND-list of required env vars. See field JSDoc.
114
+ credentialsResolvedExternally: true,
107
115
  },
108
116
  {
109
117
  name: AIProviderName.ANTHROPIC,
@@ -241,6 +249,8 @@ export const PROVIDER_DESCRIPTORS = [
241
249
  setupUrl: "https://docs.litellm.ai/docs/proxy/quick_start",
242
250
  timeouts: { generateMs: 300_000, streamMs: 120_000 },
243
251
  autoSelectPriority: 1,
252
+ // Documented zero-config local proxy — see field JSDoc.
253
+ credentialsResolvedExternally: true,
244
254
  },
245
255
  {
246
256
  name: AIProviderName.SAGEMAKER,
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Error codes that indicate a transient transport failure (dropped
3
+ * connection, socket reset, connect timeout) rather than a permanent
4
+ * misconfiguration. Shared between `proxy/proxyFetch.ts` (retry gating) and
5
+ * `utils/errorClassifier.ts` (NetworkError classification) so both stay in
6
+ * sync — a second hand-maintained copy would drift the two apart the same
7
+ * way the "5xx literal text vs statusCode" split did.
8
+ *
9
+ * undici's native `fetch()` wraps the real transport failure in
10
+ * `TypeError: fetch failed`, with the actionable code on `error.cause`
11
+ * (sometimes nested another level deep, e.g. a SocketError inside a
12
+ * ConnectTimeoutError) — never on the outer TypeError itself.
13
+ */
14
+ export declare const TRANSIENT_NETWORK_CODES: ReadonlySet<string>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Error codes that indicate a transient transport failure (dropped
3
+ * connection, socket reset, connect timeout) rather than a permanent
4
+ * misconfiguration. Shared between `proxy/proxyFetch.ts` (retry gating) and
5
+ * `utils/errorClassifier.ts` (NetworkError classification) so both stay in
6
+ * sync — a second hand-maintained copy would drift the two apart the same
7
+ * way the "5xx literal text vs statusCode" split did.
8
+ *
9
+ * undici's native `fetch()` wraps the real transport failure in
10
+ * `TypeError: fetch failed`, with the actionable code on `error.cause`
11
+ * (sometimes nested another level deep, e.g. a SocketError inside a
12
+ * ConnectTimeoutError) — never on the outer TypeError itself.
13
+ */
14
+ export const TRANSIENT_NETWORK_CODES = new Set([
15
+ "ECONNRESET",
16
+ "ETIMEDOUT",
17
+ "ECONNREFUSED",
18
+ "EPIPE",
19
+ "UND_ERR_SOCKET",
20
+ "UND_ERR_CONNECT_TIMEOUT",
21
+ ]);
22
+ //# sourceMappingURL=networkErrorCodes.js.map
@@ -26,6 +26,9 @@ export const PROVIDER_DESCRIPTORS = [
26
26
  timeouts: { generateMs: 45_000, streamMs: 120_000 },
27
27
  autoSelectPriority: 7,
28
28
  apiKeyFormatPattern: API_KEY_FORMATS.bedrock,
29
+ // Falls back to the AWS SDK's own default credential chain (shared
30
+ // profile, IAM role) when these env vars are absent — see field JSDoc.
31
+ credentialsResolvedExternally: true,
29
32
  },
30
33
  {
31
34
  name: AIProviderName.OPENAI,
@@ -89,12 +92,14 @@ export const PROVIDER_DESCRIPTORS = [
89
92
  // priority than GOOGLE_APPLICATION_CREDENTIALS by the real gating
90
93
  // logic (hasGoogleCredentials() in googleVertex/client.ts and
91
94
  // googleVertex/utils.ts) — corrected here vs. the plan snippet, which
92
- // omitted it.
95
+ // omitted it. GOOGLE_AUTH_CLIENT_EMAIL and GOOGLE_AUTH_PRIVATE_KEY are
96
+ // nested together because hasGoogleCredentials() only accepts them as
97
+ // a pair — either alone is not valid auth, unlike the other flat
98
+ // entries here which are each independently sufficient.
93
99
  extraRequiredFallbacks: [
94
100
  "GOOGLE_APPLICATION_CREDENTIALS_NEUROLINK",
95
101
  "GOOGLE_SERVICE_ACCOUNT_KEY",
96
- "GOOGLE_AUTH_CLIENT_EMAIL",
97
- "GOOGLE_AUTH_PRIVATE_KEY",
102
+ ["GOOGLE_AUTH_CLIENT_EMAIL", "GOOGLE_AUTH_PRIVATE_KEY"],
98
103
  ],
99
104
  },
100
105
  defaultModel: VertexModels.CLAUDE_4_6_SONNET,
@@ -104,6 +109,9 @@ export const PROVIDER_DESCRIPTORS = [
104
109
  setupUrl: "https://console.cloud.google.com/",
105
110
  timeouts: { generateMs: 60_000, streamMs: 120_000 },
106
111
  autoSelectPriority: 3,
112
+ // OR-of-multiple-auth-paths (file / individual fields / base64 key) —
113
+ // not a flat AND-list of required env vars. See field JSDoc.
114
+ credentialsResolvedExternally: true,
107
115
  },
108
116
  {
109
117
  name: AIProviderName.ANTHROPIC,
@@ -241,6 +249,8 @@ export const PROVIDER_DESCRIPTORS = [
241
249
  setupUrl: "https://docs.litellm.ai/docs/proxy/quick_start",
242
250
  timeouts: { generateMs: 300_000, streamMs: 120_000 },
243
251
  autoSelectPriority: 1,
252
+ // Documented zero-config local proxy — see field JSDoc.
253
+ credentialsResolvedExternally: true,
244
254
  },
245
255
  {
246
256
  name: AIProviderName.SAGEMAKER,
@@ -45,6 +45,23 @@
45
45
  */
46
46
  import { FileErrorCode } from "../errors/index.js";
47
47
  import type { BatchProcessingSummary, FileInfo, FileProcessingError, ProcessorFileProcessingResult, FileProcessorConfig, ProcessorOperationResult, ProcessedFileBase, ProcessOptions } from "../../types/index.js";
48
+ /**
49
+ * Marker on the error raised when a processor refuses content for exceeding
50
+ * the size limit *after* the download — a ZIP entry that expands past the
51
+ * bound, say.
52
+ *
53
+ * Separate from DOWNLOAD_TOO_LARGE because the two fire at different stages,
54
+ * but it exists for the same reason. Without it a bound that fires reaches
55
+ * `buildProcessedResultWithResult` as a plain Error and becomes
56
+ * PROCESSING_FAILED, which is `retryable: true` — so a caller retries a
57
+ * deterministic refusal three times and gets the identical answer each time.
58
+ * FILE_TOO_LARGE is `retryable: false`, which is the truth about this failure.
59
+ */
60
+ export declare const CONTENT_TOO_LARGE_CODE = "CONTENT_TOO_LARGE";
61
+ /** An error that a processor's own size bound was exceeded. */
62
+ export declare function contentTooLargeError(message: string): Error;
63
+ /** Whether `error` is a processor size bound firing rather than a fault. */
64
+ export declare function isContentTooLarge(error: unknown): boolean;
48
65
  /**
49
66
  * Abstract base class for file processors.
50
67
  * Provides common download, validation, and error handling functionality.
@@ -72,6 +72,29 @@ function downloadTooLargeError(maxSizeMB, typeName) {
72
72
  function isDownloadTooLarge(error) {
73
73
  return (error?.code === DOWNLOAD_TOO_LARGE_CODE);
74
74
  }
75
+ /**
76
+ * Marker on the error raised when a processor refuses content for exceeding
77
+ * the size limit *after* the download — a ZIP entry that expands past the
78
+ * bound, say.
79
+ *
80
+ * Separate from DOWNLOAD_TOO_LARGE because the two fire at different stages,
81
+ * but it exists for the same reason. Without it a bound that fires reaches
82
+ * `buildProcessedResultWithResult` as a plain Error and becomes
83
+ * PROCESSING_FAILED, which is `retryable: true` — so a caller retries a
84
+ * deterministic refusal three times and gets the identical answer each time.
85
+ * FILE_TOO_LARGE is `retryable: false`, which is the truth about this failure.
86
+ */
87
+ export const CONTENT_TOO_LARGE_CODE = "CONTENT_TOO_LARGE";
88
+ /** An error that a processor's own size bound was exceeded. */
89
+ export function contentTooLargeError(message) {
90
+ const error = new Error(message);
91
+ error.code = CONTENT_TOO_LARGE_CODE;
92
+ return error;
93
+ }
94
+ /** Whether `error` is a processor size bound firing rather than a fault. */
95
+ export function isContentTooLarge(error) {
96
+ return (error?.code === CONTENT_TOO_LARGE_CODE);
97
+ }
75
98
  /** Node's signal that a zlib output bound was reached. */
76
99
  function isBufferTooLargeError(error) {
77
100
  return (error?.code === "ERR_BUFFER_TOO_LARGE");
@@ -360,6 +383,23 @@ export class BaseFileProcessor {
360
383
  return { success: true, data: result };
361
384
  }
362
385
  catch (error) {
386
+ // A size bound firing is a verdict, not a fault: PROCESSING_FAILED is
387
+ // retryable, and re-running a decompression that will hit the same
388
+ // ceiling is the one thing worth not doing three times.
389
+ if (isContentTooLarge(error)) {
390
+ return {
391
+ success: false,
392
+ // Same detail keys as the other FILE_TOO_LARGE sites, so anything
393
+ // reading them does not have to special-case where the verdict came
394
+ // from. `sizeMB` is absent on purpose: the decompressed size is the
395
+ // number this bound exists to never find out.
396
+ error: this.createError(FileErrorCode.FILE_TOO_LARGE, {
397
+ maxMB: this.config.maxSizeMB,
398
+ type: this.config.fileTypeName,
399
+ reason: error instanceof Error ? error.message : undefined,
400
+ }),
401
+ };
402
+ }
363
403
  return {
364
404
  success: false,
365
405
  error: this.createError(FileErrorCode.PROCESSING_FAILED, { fileType: this.config.fileTypeName }, error instanceof Error ? error : undefined),
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { createRequire } from "node:module";
10
10
  import * as zlib from "node:zlib";
11
- import { BaseFileProcessor } from "../base/BaseFileProcessor.js";
11
+ import { BaseFileProcessor, contentTooLargeError, isContentTooLarge, } from "../base/BaseFileProcessor.js";
12
12
  import { readZipEntryWithinLimit } from "../archive/zipEntryReader.js";
13
13
  import { SIZE_LIMITS } from "../config/index.js";
14
14
  const require = createRequire(import.meta.url);
@@ -71,7 +71,10 @@ export class OpenDocumentProcessor extends BaseFileProcessor {
71
71
  // `maxSizeMB` only ever saw the compressed archive on the way in.
72
72
  const read = readZipEntryWithinLimit(contentEntry, SIZE_LIMITS.DOCUMENT_MAX_MB * 1024 * 1024, zlib);
73
73
  if (read.status === "too-large") {
74
- throw new Error(`content.xml exceeds the ${SIZE_LIMITS.DOCUMENT_MAX_MB}MB limit for OpenDocument content`);
74
+ // Coded, so the bound surfaces as FILE_TOO_LARGE (not retryable)
75
+ // rather than PROCESSING_FAILED (retryable). The file will be
76
+ // exactly as oversized on the next attempt.
77
+ throw contentTooLargeError(`content.xml in "${this.getFilename(fileInfo)}" exceeds the ${SIZE_LIMITS.DOCUMENT_MAX_MB}MB limit for OpenDocument content`);
75
78
  }
76
79
  if (read.status !== "ok") {
77
80
  throw new Error("content.xml could not be read from the archive");
@@ -93,6 +96,13 @@ export class OpenDocumentProcessor extends BaseFileProcessor {
93
96
  }
94
97
  }
95
98
  catch (error) {
99
+ // A size verdict passes through intact. Re-wrapping it as a plain Error
100
+ // drops the code, and the base class then reports PROCESSING_FAILED —
101
+ // which is retryable, so the caller would refetch and decompress a file
102
+ // guaranteed to be exactly as oversized.
103
+ if (isContentTooLarge(error)) {
104
+ throw error;
105
+ }
96
106
  const message = error instanceof Error ? error.message : "Unknown error";
97
107
  throw new Error(`Failed to extract OpenDocument content: ${message}`, {
98
108
  cause: error,
@@ -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,
@@ -1714,8 +1714,8 @@ export type ProviderDescriptor = {
1714
1714
  modelFallbacks?: readonly string[];
1715
1715
  /** Additional env vars required alongside apiKey (e.g. AWS secret key, Azure endpoint). */
1716
1716
  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[];
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). 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. */
1718
+ extraRequiredFallbacks?: readonly (string | readonly string[])[];
1719
1719
  /** 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
1720
  optional?: boolean;
1721
1721
  };
@@ -1741,6 +1741,21 @@ export type ProviderDescriptor = {
1741
1741
  autoSelectPriority?: number;
1742
1742
  /** Format-validation regex sourced from providerConfig.ts's API_KEY_FORMATS, when one exists for this provider. */
1743
1743
  apiKeyFormatPattern?: RegExp;
1744
+ /**
1745
+ * True when this provider's credentials are resolved by an external chain
1746
+ * or its own config validator rather than by plain env-var presence, so
1747
+ * its required-env-vars can't be expressed as "every one of these exact
1748
+ * names must be literally set". Examples: Vertex accepts a service-account
1749
+ * file OR individual client-email/private-key fields OR a base64 key
1750
+ * (an OR, not an AND, of auth paths); Bedrock falls back to the AWS SDK's
1751
+ * own default credential chain (shared profile, IAM role) with no env
1752
+ * vars required at all; LiteLLM is a documented zero-config local proxy.
1753
+ * `ProviderHealthChecker.getRequiredEnvironmentVariables()` returns `[]`
1754
+ * for these providers and defers to `checkProviderSpecificConfig()`'s
1755
+ * dedicated per-provider check instead of deriving a flat AND-list from
1756
+ * `envVars`.
1757
+ */
1758
+ credentialsResolvedExternally?: boolean;
1744
1759
  };
1745
1760
  /** Minimal NeuroLink-like instance accepted by the image generation service. */
1746
1761
  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
  },
@@ -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