@juspay/neurolink 10.12.9 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/browser/neurolink.min.js +504 -506
  3. package/dist/cli/commands/setup.d.ts +20 -1
  4. package/dist/cli/commands/setup.js +160 -36
  5. package/dist/cli/factories/commandFactory.d.ts +1157 -1
  6. package/dist/cli/factories/commandFactory.js +20 -53
  7. package/dist/core/baseProvider.js +3 -3
  8. package/dist/core/dynamicModels.d.ts +1 -1
  9. package/dist/core/dynamicModels.js +1 -23
  10. package/dist/core/modelConfiguration.d.ts +1 -70
  11. package/dist/core/modelConfiguration.js +0 -303
  12. package/dist/core/modules/GenerationHandler.js +2 -2
  13. package/dist/core/modules/structuredOutputPolicy.d.ts +2 -2
  14. package/dist/core/modules/structuredOutputPolicy.js +2 -2
  15. package/dist/factories/providerDescriptors.d.ts +18 -0
  16. package/dist/factories/providerDescriptors.js +536 -0
  17. package/dist/factories/providerFactory.d.ts +28 -2
  18. package/dist/factories/providerFactory.js +53 -42
  19. package/dist/factories/providerRegistry.js +33 -32
  20. package/dist/index.d.ts +10 -1
  21. package/dist/index.js +13 -3
  22. package/dist/lib/core/baseProvider.js +3 -3
  23. package/dist/lib/core/dynamicModels.d.ts +1 -1
  24. package/dist/lib/core/dynamicModels.js +1 -23
  25. package/dist/lib/core/modelConfiguration.d.ts +1 -70
  26. package/dist/lib/core/modelConfiguration.js +0 -303
  27. package/dist/lib/core/modules/GenerationHandler.js +2 -2
  28. package/dist/lib/core/modules/structuredOutputPolicy.d.ts +2 -2
  29. package/dist/lib/core/modules/structuredOutputPolicy.js +2 -2
  30. package/dist/lib/factories/providerDescriptors.d.ts +18 -0
  31. package/dist/lib/factories/providerDescriptors.js +537 -0
  32. package/dist/lib/factories/providerFactory.d.ts +28 -2
  33. package/dist/lib/factories/providerFactory.js +53 -42
  34. package/dist/lib/factories/providerRegistry.js +33 -32
  35. package/dist/lib/index.d.ts +10 -1
  36. package/dist/lib/index.js +13 -3
  37. package/dist/lib/models/anthropicModels.d.ts +0 -7
  38. package/dist/lib/models/anthropicModels.js +0 -9
  39. package/dist/lib/neurolink.js +25 -37
  40. package/dist/lib/providers/amazonBedrock/client.js +34 -16
  41. package/dist/lib/providers/anthropic/client.js +39 -34
  42. package/dist/lib/providers/anthropic/constants.d.ts +0 -1
  43. package/dist/lib/providers/anthropic/constants.js +0 -2
  44. package/dist/lib/providers/anthropic/index.d.ts +0 -1
  45. package/dist/lib/providers/anthropic/index.js +0 -1
  46. package/dist/lib/providers/azureOpenai.js +17 -15
  47. package/dist/lib/providers/cloudflare.js +12 -21
  48. package/dist/lib/providers/cohere.js +31 -25
  49. package/dist/lib/providers/deepseek.js +23 -26
  50. package/dist/lib/providers/fireworks.js +12 -21
  51. package/dist/lib/providers/googleAiStudio/client.d.ts +2 -0
  52. package/dist/lib/providers/googleAiStudio/client.js +39 -17
  53. package/dist/lib/providers/googleAiStudio/index.d.ts +0 -1
  54. package/dist/lib/providers/googleAiStudio/index.js +0 -1
  55. package/dist/lib/providers/googleNativeGemini3/index.d.ts +0 -1
  56. package/dist/lib/providers/googleNativeGemini3/index.js +0 -1
  57. package/dist/lib/providers/googleVertex/client.d.ts +0 -51
  58. package/dist/lib/providers/googleVertex/client.js +107 -777
  59. package/dist/lib/providers/groq.js +19 -22
  60. package/dist/lib/providers/huggingFace/client.d.ts +1 -1
  61. package/dist/lib/providers/huggingFace/client.js +27 -25
  62. package/dist/lib/providers/huggingFace/index.d.ts +0 -1
  63. package/dist/lib/providers/huggingFace/index.js +0 -1
  64. package/dist/lib/providers/jina.d.ts +0 -1
  65. package/dist/lib/providers/jina.js +0 -1
  66. package/dist/lib/providers/litellm/client.js +54 -39
  67. package/dist/lib/providers/litellm/index.d.ts +0 -2
  68. package/dist/lib/providers/litellm/index.js +0 -2
  69. package/dist/lib/providers/llamaCpp.d.ts +1 -0
  70. package/dist/lib/providers/llamaCpp.js +24 -18
  71. package/dist/lib/providers/lmStudio.js +25 -42
  72. package/dist/lib/providers/mistral.js +12 -24
  73. package/dist/lib/providers/nvidiaNim/client.js +37 -34
  74. package/dist/lib/providers/nvidiaNim/index.d.ts +0 -1
  75. package/dist/lib/providers/nvidiaNim/index.js +0 -1
  76. package/dist/lib/providers/ollama/client.js +51 -58
  77. package/dist/lib/providers/ollama/index.d.ts +0 -1
  78. package/dist/lib/providers/ollama/index.js +0 -1
  79. package/dist/lib/providers/openAI/client.js +37 -40
  80. package/dist/lib/providers/openAI/index.d.ts +0 -2
  81. package/dist/lib/providers/openAI/index.js +0 -2
  82. package/dist/lib/providers/openRouter/client.js +53 -47
  83. package/dist/lib/providers/openRouter/utils.d.ts +0 -2
  84. package/dist/lib/providers/openRouter/utils.js +0 -12
  85. package/dist/lib/providers/openaiChatCompletionsBase.d.ts +9 -0
  86. package/dist/lib/providers/openaiChatCompletionsBase.js +73 -16
  87. package/dist/lib/providers/openaiCompatible/client.js +36 -32
  88. package/dist/lib/providers/perplexity.d.ts +2 -1
  89. package/dist/lib/providers/perplexity.js +14 -22
  90. package/dist/lib/providers/replicate.d.ts +0 -1
  91. package/dist/lib/providers/replicate.js +22 -9
  92. package/dist/lib/providers/togetherAi.js +12 -21
  93. package/dist/lib/providers/voyage.d.ts +0 -1
  94. package/dist/lib/providers/voyage.js +0 -1
  95. package/dist/lib/providers/xai.js +17 -26
  96. package/dist/lib/server/errors.d.ts +1 -1
  97. package/dist/lib/server/errors.js +2 -2
  98. package/dist/lib/server/index.d.ts +2 -2
  99. package/dist/lib/server/index.js +5 -3
  100. package/dist/lib/server/middleware/rateLimit.d.ts +0 -4
  101. package/dist/lib/server/middleware/rateLimit.js +0 -4
  102. package/dist/lib/types/errors.d.ts +35 -0
  103. package/dist/lib/types/index.d.ts +0 -1
  104. package/dist/lib/types/index.js +0 -1
  105. package/dist/lib/types/providers.d.ts +66 -11
  106. package/dist/lib/utils/errorClassifier.d.ts +30 -0
  107. package/dist/lib/utils/errorClassifier.js +94 -0
  108. package/dist/lib/utils/fileDetector.js +6 -43
  109. package/dist/lib/utils/providerConfig.d.ts +0 -8
  110. package/dist/lib/utils/providerConfig.js +0 -37
  111. package/dist/lib/utils/providerHealth.d.ts +42 -7
  112. package/dist/lib/utils/providerHealth.js +115 -122
  113. package/dist/lib/utils/providerUtils.js +21 -87
  114. package/dist/models/anthropicModels.d.ts +0 -7
  115. package/dist/models/anthropicModels.js +0 -9
  116. package/dist/neurolink.js +25 -37
  117. package/dist/providers/amazonBedrock/client.js +34 -16
  118. package/dist/providers/amazonSagemaker.d.ts +1 -1
  119. package/dist/providers/anthropic/client.js +39 -34
  120. package/dist/providers/anthropic/constants.d.ts +0 -1
  121. package/dist/providers/anthropic/constants.js +0 -2
  122. package/dist/providers/anthropic/index.d.ts +0 -1
  123. package/dist/providers/anthropic/index.js +0 -1
  124. package/dist/providers/azureOpenai.js +17 -15
  125. package/dist/providers/cloudflare.js +12 -21
  126. package/dist/providers/cohere.js +31 -25
  127. package/dist/providers/deepseek.js +23 -26
  128. package/dist/providers/fireworks.js +12 -21
  129. package/dist/providers/googleAiStudio/client.d.ts +2 -0
  130. package/dist/providers/googleAiStudio/client.js +39 -17
  131. package/dist/providers/googleAiStudio/index.d.ts +0 -1
  132. package/dist/providers/googleAiStudio/index.js +0 -1
  133. package/dist/providers/googleNativeGemini3/index.d.ts +0 -1
  134. package/dist/providers/googleNativeGemini3/index.js +0 -1
  135. package/dist/providers/googleVertex/client.d.ts +0 -51
  136. package/dist/providers/googleVertex/client.js +107 -777
  137. package/dist/providers/groq.js +19 -22
  138. package/dist/providers/huggingFace/client.d.ts +1 -1
  139. package/dist/providers/huggingFace/client.js +27 -25
  140. package/dist/providers/huggingFace/index.d.ts +0 -1
  141. package/dist/providers/huggingFace/index.js +0 -1
  142. package/dist/providers/jina.d.ts +0 -1
  143. package/dist/providers/jina.js +0 -1
  144. package/dist/providers/litellm/client.js +54 -39
  145. package/dist/providers/litellm/index.d.ts +0 -2
  146. package/dist/providers/litellm/index.js +0 -2
  147. package/dist/providers/llamaCpp.d.ts +1 -0
  148. package/dist/providers/llamaCpp.js +24 -18
  149. package/dist/providers/lmStudio.js +25 -42
  150. package/dist/providers/mistral.js +12 -24
  151. package/dist/providers/nvidiaNim/client.js +37 -34
  152. package/dist/providers/nvidiaNim/index.d.ts +0 -1
  153. package/dist/providers/nvidiaNim/index.js +0 -1
  154. package/dist/providers/ollama/client.js +51 -58
  155. package/dist/providers/ollama/index.d.ts +0 -1
  156. package/dist/providers/ollama/index.js +0 -1
  157. package/dist/providers/openAI/client.js +37 -40
  158. package/dist/providers/openAI/index.d.ts +0 -2
  159. package/dist/providers/openAI/index.js +0 -2
  160. package/dist/providers/openRouter/client.js +53 -47
  161. package/dist/providers/openRouter/utils.d.ts +0 -2
  162. package/dist/providers/openRouter/utils.js +0 -12
  163. package/dist/providers/openaiChatCompletionsBase.d.ts +9 -0
  164. package/dist/providers/openaiChatCompletionsBase.js +73 -16
  165. package/dist/providers/openaiCompatible/client.js +36 -32
  166. package/dist/providers/perplexity.d.ts +2 -1
  167. package/dist/providers/perplexity.js +14 -22
  168. package/dist/providers/replicate.d.ts +0 -1
  169. package/dist/providers/replicate.js +22 -9
  170. package/dist/providers/sagemaker/language-model.d.ts +2 -2
  171. package/dist/providers/togetherAi.js +12 -21
  172. package/dist/providers/voyage.d.ts +0 -1
  173. package/dist/providers/voyage.js +0 -1
  174. package/dist/providers/xai.js +17 -26
  175. package/dist/server/errors.d.ts +1 -1
  176. package/dist/server/errors.js +2 -2
  177. package/dist/server/index.d.ts +2 -2
  178. package/dist/server/index.js +5 -3
  179. package/dist/server/middleware/rateLimit.d.ts +0 -4
  180. package/dist/server/middleware/rateLimit.js +0 -4
  181. package/dist/types/errors.d.ts +35 -0
  182. package/dist/types/index.d.ts +0 -1
  183. package/dist/types/index.js +0 -1
  184. package/dist/types/providers.d.ts +66 -11
  185. package/dist/utils/errorClassifier.d.ts +30 -0
  186. package/dist/utils/errorClassifier.js +93 -0
  187. package/dist/utils/fileDetector.js +6 -43
  188. package/dist/utils/providerConfig.d.ts +0 -8
  189. package/dist/utils/providerConfig.js +0 -37
  190. package/dist/utils/providerHealth.d.ts +42 -7
  191. package/dist/utils/providerHealth.js +115 -122
  192. package/dist/utils/providerUtils.js +21 -87
  193. package/package.json +21 -75
  194. package/dist/lib/providers/anthropic/utils.d.ts +0 -7
  195. package/dist/lib/providers/anthropic/utils.js +0 -150
  196. package/dist/lib/providers/googleAiStudio/utils.d.ts +0 -2
  197. package/dist/lib/providers/googleAiStudio/utils.js +0 -19
  198. package/dist/lib/providers/googleNativeGemini3/constants.d.ts +0 -1
  199. package/dist/lib/providers/googleNativeGemini3/constants.js +0 -2
  200. package/dist/lib/providers/huggingFace/utils.d.ts +0 -2
  201. package/dist/lib/providers/huggingFace/utils.js +0 -8
  202. package/dist/lib/providers/index.d.ts +0 -33
  203. package/dist/lib/providers/index.js +0 -34
  204. package/dist/lib/providers/litellm/constants.d.ts +0 -1
  205. package/dist/lib/providers/litellm/constants.js +0 -3
  206. package/dist/lib/providers/litellm/utils.d.ts +0 -5
  207. package/dist/lib/providers/litellm/utils.js +0 -11
  208. package/dist/lib/providers/nvidiaNim/utils.d.ts +0 -9
  209. package/dist/lib/providers/nvidiaNim/utils.js +0 -101
  210. package/dist/lib/providers/ollama/utils.d.ts +0 -8
  211. package/dist/lib/providers/ollama/utils.js +0 -43
  212. package/dist/lib/providers/openAI/constants.d.ts +0 -1
  213. package/dist/lib/providers/openAI/constants.js +0 -3
  214. package/dist/lib/providers/openAI/utils.d.ts +0 -2
  215. package/dist/lib/providers/openAI/utils.js +0 -8
  216. package/dist/lib/types/universalProviderOptions.d.ts +0 -89
  217. package/dist/lib/types/universalProviderOptions.js +0 -55
  218. package/dist/providers/anthropic/utils.d.ts +0 -7
  219. package/dist/providers/anthropic/utils.js +0 -149
  220. package/dist/providers/googleAiStudio/utils.d.ts +0 -2
  221. package/dist/providers/googleAiStudio/utils.js +0 -18
  222. package/dist/providers/googleNativeGemini3/constants.d.ts +0 -1
  223. package/dist/providers/googleNativeGemini3/constants.js +0 -1
  224. package/dist/providers/huggingFace/utils.d.ts +0 -2
  225. package/dist/providers/huggingFace/utils.js +0 -7
  226. package/dist/providers/index.d.ts +0 -33
  227. package/dist/providers/index.js +0 -33
  228. package/dist/providers/litellm/constants.d.ts +0 -1
  229. package/dist/providers/litellm/constants.js +0 -2
  230. package/dist/providers/litellm/utils.d.ts +0 -5
  231. package/dist/providers/litellm/utils.js +0 -10
  232. package/dist/providers/nvidiaNim/utils.d.ts +0 -9
  233. package/dist/providers/nvidiaNim/utils.js +0 -100
  234. package/dist/providers/ollama/utils.d.ts +0 -8
  235. package/dist/providers/ollama/utils.js +0 -42
  236. package/dist/providers/openAI/constants.d.ts +0 -1
  237. package/dist/providers/openAI/constants.js +0 -2
  238. package/dist/providers/openAI/utils.d.ts +0 -2
  239. package/dist/providers/openAI/utils.js +0 -7
  240. package/dist/types/universalProviderOptions.d.ts +0 -89
  241. package/dist/types/universalProviderOptions.js +0 -54
@@ -1,6 +1,7 @@
1
1
  import { modelConfig } from "../../core/modelConfiguration.js";
2
2
  import { createProxyFetch } from "../../proxy/proxyFetch.js";
3
3
  import { InvalidModelError, NetworkError, ProviderError, } from "../../types/index.js";
4
+ import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../../utils/errorClassifier.js";
4
5
  import { logger } from "../../utils/logger.js";
5
6
  import { redactUrlCredentials } from "../../utils/logSanitize.js";
6
7
  import { createTimeoutController, parseTimeout, TimeoutError, } from "../../utils/timeout.js";
@@ -81,47 +82,61 @@ export class OllamaProvider extends OpenAIChatCompletionsProvider {
81
82
  }
82
83
  formatProviderError(error) {
83
84
  if (error instanceof TimeoutError) {
85
+ // Custom message (not classifyProviderError's built-in "Request timed
86
+ // out: ..." default). TimeoutError is handled unconditionally inside
87
+ // classifyProviderError ahead of any rule table, so this quirk can only
88
+ // be preserved via a pre-delegate intercept (same pattern used for
89
+ // groq's TimeoutError override).
84
90
  return new NetworkError(`Ollama request timed out. The model may be loading or the request is too large.`, "ollama");
85
91
  }
92
+ // `responseBody` isn't part of ProviderErrorContext, so it's read off the
93
+ // raw error here (mirrors openAI's `errorType` extraction) for the
94
+ // missing-model / 404 rules below, which match against message+body
95
+ // combined exactly as the pre-migration code did.
86
96
  const errorRecord = error;
87
- const message = typeof errorRecord?.message === "string"
88
- ? errorRecord.message
89
- : "Unknown error";
90
- const cause = errorRecord?.cause ?? {};
91
- const code = (errorRecord?.code ?? cause?.code);
92
- if (code === "ECONNREFUSED" ||
93
- message.includes("ECONNREFUSED") ||
94
- message.includes("Failed to fetch") ||
95
- message.includes("fetch failed")) {
96
- return new NetworkError(`Cannot connect to Ollama at ${redactUrlCredentials(this.config.baseURL)}. ` +
97
- `Install Ollama (https://ollama.com), start it with 'ollama serve', then try again.`, "ollama");
98
- }
99
- // The base client (buildAPIError) attaches statusCode + responseBody to
100
- // HTTP failures. Distinguish a genuine missing-model error (give 'ollama
101
- // pull' guidance) from a bare endpoint-mismatch 404 (wrong base URL / not
102
- // the OpenAI-compatible /v1 surface) so the advice is actionable. Match on
103
- // wording, not a bare "404" substring, to avoid misclassifying unrelated
104
- // messages that merely contain those digits.
105
- const statusCode = typeof errorRecord?.statusCode === "number"
106
- ? errorRecord.statusCode
107
- : undefined;
108
97
  const responseBody = typeof errorRecord?.responseBody === "string"
109
98
  ? errorRecord.responseBody
110
99
  : "";
111
- const haystack = `${message} ${responseBody}`.toLowerCase();
112
- const looksLikeMissingModel = haystack.includes("model_not_found") ||
113
- (haystack.includes("model") && haystack.includes("not found"));
114
- if (looksLikeMissingModel) {
115
- return new InvalidModelError(`Ollama model '${this.modelName}' is not available locally. ` +
116
- `Pull it first with 'ollama pull ${this.modelName}' (or try '${FALLBACK_OLLAMA_MODEL}'); ` +
117
- `list installed models with 'ollama list'.`, "ollama");
118
- }
119
- if (statusCode === 404 || haystack.includes("status 404")) {
120
- return new ProviderError(`Ollama returned HTTP 404 from ${redactUrlCredentials(this.config.baseURL)}. ` +
121
- `Verify the base URL serves the OpenAI-compatible /v1 API and that the ` +
122
- `model is installed ('ollama list').`, "ollama");
123
- }
124
- return new ProviderError(`Ollama error: ${message}`, "ollama");
100
+ const cause = errorRecord?.cause ?? {};
101
+ const code = (errorRecord?.code ?? cause?.code);
102
+ const rules = [
103
+ {
104
+ match: (ctx) => code === "ECONNREFUSED" ||
105
+ /ECONNREFUSED|Failed to fetch|fetch failed/.test(ctx.message),
106
+ errorClass: NetworkError,
107
+ message: () => `Cannot connect to Ollama at ${redactUrlCredentials(this.config.baseURL)}. ` +
108
+ `Install Ollama (https://ollama.com), start it with 'ollama serve', then try again.`,
109
+ },
110
+ // The base client (buildAPIError) attaches statusCode + responseBody to
111
+ // HTTP failures. Distinguish a genuine missing-model error (give 'ollama
112
+ // pull' guidance) from a bare endpoint-mismatch 404 (wrong base URL / not
113
+ // the OpenAI-compatible /v1 surface) so the advice is actionable. Match
114
+ // on wording, not a bare "404" substring, to avoid misclassifying
115
+ // unrelated messages that merely contain those digits.
116
+ {
117
+ match: (ctx) => {
118
+ const haystack = `${ctx.message} ${responseBody}`.toLowerCase();
119
+ return (haystack.includes("model_not_found") ||
120
+ (haystack.includes("model") && haystack.includes("not found")));
121
+ },
122
+ errorClass: InvalidModelError,
123
+ message: () => `Ollama model '${this.modelName}' is not available locally. ` +
124
+ `Pull it first with 'ollama pull ${this.modelName}' (or try '${FALLBACK_OLLAMA_MODEL}'); ` +
125
+ `list installed models with 'ollama list'.`,
126
+ },
127
+ {
128
+ match: (ctx) => {
129
+ const haystack = `${ctx.message} ${responseBody}`.toLowerCase();
130
+ return ctx.statusCode === 404 || haystack.includes("status 404");
131
+ },
132
+ errorClass: ProviderError,
133
+ message: () => `Ollama returned HTTP 404 from ${redactUrlCredentials(this.config.baseURL)}. ` +
134
+ `Verify the base URL serves the OpenAI-compatible /v1 API and that the ` +
135
+ `model is installed ('ollama list').`,
136
+ },
137
+ ...DEFAULT_ERROR_RULES,
138
+ ];
139
+ return classifyProviderError(error, rules, "ollama", this.modelName);
125
140
  }
126
141
  // ===========================================================================
127
142
  // Optional hooks — Ollama-specific behaviour
@@ -181,29 +196,7 @@ export class OllamaProvider extends OpenAIChatCompletionsProvider {
181
196
  * back to a model that the first real request can't serve, so report unusable.
182
197
  */
183
198
  async validateConfiguration() {
184
- try {
185
- const url = `${stripTrailingSlash(this.config.baseURL)}/models`;
186
- const proxyFetch = createProxyFetch();
187
- const r = await proxyFetch(url, {
188
- headers: {
189
- ...this.getAuthHeaders(),
190
- "Content-Type": "application/json",
191
- },
192
- signal: AbortSignal.timeout(5000),
193
- });
194
- if (!r.ok) {
195
- return false;
196
- }
197
- const data = (await r.json().catch(() => null));
198
- return Boolean(data?.data?.some((m) => typeof m?.id === "string" && m.id.trim().length > 0));
199
- }
200
- catch (error) {
201
- logger.debug("Ollama validateConfiguration probe failed", {
202
- baseURL: redactUrlCredentials(this.config.baseURL),
203
- error: error instanceof Error ? error.message : String(error),
204
- });
205
- return false;
206
- }
199
+ return this.probeModelsEndpoint(this.getAuthHeaders());
207
200
  }
208
201
  getConfiguration() {
209
202
  return {
@@ -1,3 +1,2 @@
1
1
  export * from "./client.js";
2
2
  export * from "./constants.js";
3
- export * from "./utils.js";
@@ -1,3 +1,2 @@
1
1
  export * from "./client.js";
2
2
  export * from "./constants.js";
3
- export * from "./utils.js";
@@ -1,14 +1,15 @@
1
1
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
2
2
  import { AIProviderName as AIProviderNameEnum } from "../../constants/enums.js";
3
3
  import { createProxyFetch } from "../../proxy/proxyFetch.js";
4
- import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
4
+ import { AuthenticationError, InvalidModelError, ProviderError, RateLimitError, } from "../../types/index.js";
5
5
  import { logger } from "../../utils/logger.js";
6
6
  import { redactUrlCredentials } from "../../utils/logSanitize.js";
7
7
  import { calculateCost } from "../../utils/pricing.js";
8
+ import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../../utils/errorClassifier.js";
8
9
  import { createOpenAIConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
9
10
  import { MAX_IMAGE_BYTES, readBoundedBuffer } from "../../utils/sizeGuard.js";
10
11
  import { assertSafeUrl } from "../../utils/ssrfGuard.js";
11
- import { createTimeoutController, TimeoutError } from "../../utils/timeout.js";
12
+ import { createTimeoutController } from "../../utils/timeout.js";
12
13
  import { stripTrailingSlash } from "../openaiChatCompletionsClient.js";
13
14
  import { OpenAIChatCompletionsProvider } from "../openaiChatCompletionsBase.js";
14
15
  const OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1";
@@ -89,48 +90,44 @@ export class OpenAIProvider extends OpenAIChatCompletionsProvider {
89
90
  return getOpenAIModel();
90
91
  }
91
92
  formatProviderError(error) {
92
- if (error instanceof TimeoutError) {
93
- return new NetworkError(error.message, this.providerName);
94
- }
93
+ // `type` isn't part of `ProviderErrorContext` (it's an OpenAI-specific
94
+ // error-body field), so it's read directly off the raw error here and
95
+ // captured by the rule closures below.
95
96
  const errorObj = error;
96
- const message = errorObj?.message && typeof errorObj.message === "string"
97
- ? errorObj.message
98
- : "Unknown error";
99
97
  const errorType = errorObj?.type && typeof errorObj.type === "string"
100
98
  ? errorObj.type
101
99
  : undefined;
102
- const statusCode = typeof errorObj?.status === "number"
103
- ? errorObj.status
104
- : typeof errorObj?.statusCode === "number"
105
- ? errorObj.statusCode
106
- : undefined;
107
- // Curator P1-1 / Reviewer Finding #4: only the explicit auth markers
108
- // map to AuthenticationError. Earlier we treated every
109
- // `invalid_request_error` as an auth failure — that's OpenAI's catch-all
110
- // for any bad request (unsupported parameter, malformed JSON, etc.) and
111
- // mislabelled them as "invalid API key". Use credential-specific
112
- // signals only.
113
- if (message.includes("API_KEY_INVALID") ||
114
- message.includes("Invalid API key") ||
115
- message.includes("Incorrect API key") ||
116
- message.includes("invalid_api_key") ||
117
- errorType === "invalid_api_key" ||
118
- statusCode === 401) {
119
- return new AuthenticationError(message.includes("Incorrect API key") ||
120
- message.includes("Invalid API key")
121
- ? message
122
- : "Invalid OpenAI API key. Please check your OPENAI_API_KEY environment variable.", this.providerName);
123
- }
124
- if (message.includes("rate limit") ||
125
- errorType === "rate_limit_error" ||
126
- statusCode === 429) {
127
- return new RateLimitError("OpenAI rate limit exceeded. Please try again later.", this.providerName);
128
- }
129
- if (message.includes("model_not_found")) {
130
- return new InvalidModelError(`Model not found: ${this.modelName}`, this.providerName);
131
- }
132
- // Generic provider error
133
- return new ProviderError(`OpenAI error: ${message}`, this.providerName);
100
+ const rules = [
101
+ // Curator P1-1 / Reviewer Finding #4: only the explicit auth markers
102
+ // map to AuthenticationError. Earlier we treated every
103
+ // `invalid_request_error` as an auth failure — that's OpenAI's
104
+ // catch-all for any bad request (unsupported parameter, malformed
105
+ // JSON, etc.) and mislabelled them as "invalid API key". Use
106
+ // credential-specific signals only.
107
+ {
108
+ match: (ctx) => ctx.statusCode === 401 ||
109
+ errorType === "invalid_api_key" ||
110
+ /API_KEY_INVALID|Invalid API key|Incorrect API key|invalid_api_key/i.test(ctx.message),
111
+ errorClass: AuthenticationError,
112
+ message: (ctx) => /Incorrect API key|Invalid API key/i.test(ctx.message)
113
+ ? ctx.message
114
+ : "Invalid OpenAI API key. Please check your OPENAI_API_KEY environment variable.",
115
+ },
116
+ {
117
+ match: (ctx) => ctx.statusCode === 429 ||
118
+ errorType === "rate_limit_error" ||
119
+ /rate limit/i.test(ctx.message),
120
+ errorClass: RateLimitError,
121
+ message: "OpenAI rate limit exceeded. Please try again later.",
122
+ },
123
+ {
124
+ match: (ctx) => /model_not_found/i.test(ctx.message),
125
+ errorClass: InvalidModelError,
126
+ message: (ctx) => `Model not found: ${ctx.modelName}`,
127
+ },
128
+ ...DEFAULT_ERROR_RULES,
129
+ ];
130
+ return classifyProviderError(error, rules, this.providerName, this.modelName);
134
131
  }
135
132
  // ===========================================================================
136
133
  // Optional hook overrides
@@ -1,3 +1 @@
1
1
  export * from "./client.js";
2
- export * from "./constants.js";
3
- export * from "./utils.js";
@@ -1,3 +1 @@
1
1
  export * from "./client.js";
2
- export * from "./constants.js";
3
- export * from "./utils.js";
@@ -1,10 +1,10 @@
1
1
  import { AIProviderName } from "../../constants/enums.js";
2
2
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
3
3
  import { createProxyFetch } from "../../proxy/proxyFetch.js";
4
+ import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../../utils/errorClassifier.js";
4
5
  import { isAbortError } from "../../utils/errorHandling.js";
5
6
  import { logger } from "../../utils/logger.js";
6
7
  import { redactUrlCredentials } from "../../utils/logSanitize.js";
7
- import { TimeoutError } from "../../utils/timeout.js";
8
8
  import { OpenAIChatCompletionsProvider } from "../openaiChatCompletionsBase.js";
9
9
  import { stripTrailingSlash } from "../openaiChatCompletionsClient.js";
10
10
  import { getDefaultOpenRouterModel } from "./utils.js";
@@ -87,63 +87,69 @@ export class OpenRouterProvider extends OpenAIChatCompletionsProvider {
87
87
  return getDefaultOpenRouterModel();
88
88
  }
89
89
  formatProviderError(error) {
90
- if (error instanceof TimeoutError) {
91
- return new NetworkError(`Request timed out: ${error.message}`, "openrouter");
92
- }
93
- // Check for timeout by error name and message as fallback
94
- const errorRecord = error;
95
- if (errorRecord?.name === "TimeoutError" ||
96
- (typeof errorRecord?.message === "string" &&
97
- errorRecord.message.includes("Timeout"))) {
98
- return new NetworkError(`Request timed out: ${errorRecord?.message || "Unknown timeout"}`, "openrouter");
99
- }
100
- if (typeof errorRecord?.message === "string") {
101
- if (errorRecord.message.includes("ECONNREFUSED") ||
102
- errorRecord.message.includes("Failed to fetch")) {
103
- return new NetworkError("OpenRouter API not available. Please check your network connection and try again.", "openrouter");
104
- }
105
- if (errorRecord.message.includes("API_KEY_INVALID") ||
106
- errorRecord.message.includes("Invalid API key") ||
107
- errorRecord.message.includes("invalid_api_key") ||
108
- errorRecord.message.includes("Unauthorized")) {
109
- return new AuthenticationError("Invalid OpenRouter API key. Please check your OPENROUTER_API_KEY environment variable. " +
110
- "Get your key at https://openrouter.ai/keys", "openrouter");
111
- }
112
- if (errorRecord.message.includes("rate limit")) {
113
- return new RateLimitError("OpenRouter rate limit exceeded. Please try again later or upgrade your account at https://openrouter.ai/credits", "openrouter");
114
- }
115
- if (errorRecord.message.includes("model") &&
116
- errorRecord.message.includes("not found")) {
117
- return new InvalidModelError(`Model '${this.modelName}' not available on OpenRouter. ` +
118
- "Browse available models at https://openrouter.ai/models", "openrouter");
119
- }
120
- if (errorRecord.message.includes("insufficient_credits")) {
121
- return new ProviderError("Insufficient OpenRouter credits. Add credits at https://openrouter.ai/credits", "openrouter");
122
- }
90
+ const rules = [
91
+ // Duck-typed timeout detection (name === "TimeoutError" OR message
92
+ // contains "Timeout", case-sensitive — matches the original's
93
+ // `.includes("Timeout")`) distinct from the `instanceof TimeoutError`
94
+ // check classifyProviderError already performs first.
95
+ {
96
+ match: (ctx) => ctx.errorName === "TimeoutError" || /Timeout/.test(ctx.message),
97
+ errorClass: NetworkError,
98
+ message: (ctx) => `Request timed out: ${ctx.message}`,
99
+ },
100
+ {
101
+ match: (ctx) => /ECONNREFUSED|Failed to fetch/.test(ctx.message),
102
+ errorClass: NetworkError,
103
+ message: "OpenRouter API not available. Please check your network connection and try again.",
104
+ },
105
+ {
106
+ match: (ctx) => /API_KEY_INVALID|Invalid API key|invalid_api_key|Unauthorized/.test(ctx.message),
107
+ errorClass: AuthenticationError,
108
+ message: "Invalid OpenRouter API key. Please check your OPENROUTER_API_KEY environment variable. " +
109
+ "Get your key at https://openrouter.ai/keys",
110
+ },
111
+ {
112
+ match: (ctx) => /rate limit/.test(ctx.message),
113
+ errorClass: RateLimitError,
114
+ message: "OpenRouter rate limit exceeded. Please try again later or upgrade your account at https://openrouter.ai/credits",
115
+ },
116
+ {
117
+ match: (ctx) => /model/.test(ctx.message) && /not found/.test(ctx.message),
118
+ errorClass: InvalidModelError,
119
+ message: () => `Model '${this.modelName}' not available on OpenRouter. ` +
120
+ "Browse available models at https://openrouter.ai/models",
121
+ },
122
+ {
123
+ match: (ctx) => /insufficient_credits/.test(ctx.message),
124
+ errorClass: ProviderError,
125
+ message: "Insufficient OpenRouter credits. Add credits at https://openrouter.ai/credits",
126
+ },
123
127
  // "No endpoints found" — model temporarily unavailable or unsupported
124
128
  // parameters. Distinct from tool errors: it can happen on any request
125
129
  // when the model has no available providers on OpenRouter.
126
- if (errorRecord.message.includes("No endpoints found")) {
127
- return new InvalidModelError(`No endpoints found for model '${this.modelName}' on OpenRouter. ` +
130
+ {
131
+ match: (ctx) => /No endpoints found/.test(ctx.message),
132
+ errorClass: InvalidModelError,
133
+ message: () => `No endpoints found for model '${this.modelName}' on OpenRouter. ` +
128
134
  "The model may be temporarily unavailable or does not support the requested parameters. " +
129
- "Try a different model or check availability at https://openrouter.ai/models", "openrouter");
130
- }
135
+ "Try a different model or check availability at https://openrouter.ai/models",
136
+ },
131
137
  // Tool/function calling errors
132
- if (errorRecord.message.includes("tool use") ||
133
- errorRecord.message.includes("tool_use") ||
134
- errorRecord.message.includes("function_call") ||
135
- errorRecord.message.includes("tools are not supported")) {
136
- return new ProviderError(`Model '${this.modelName}' does not support tool calling. ` +
138
+ {
139
+ match: (ctx) => /tool use|tool_use|function_call|tools are not supported/.test(ctx.message),
140
+ errorClass: ProviderError,
141
+ message: () => `Model '${this.modelName}' does not support tool calling. ` +
137
142
  "Use a tool-capable model like:\n" +
138
143
  " • google/gemini-2.0-flash-exp:free (free)\n" +
139
144
  " • meta-llama/llama-3.3-70b-instruct:free (free)\n" +
140
145
  " • anthropic/claude-3.7-sonnet (paid)\n" +
141
146
  " • openai/gpt-4o (paid)\n" +
142
147
  "Or use --disableTools flag. " +
143
- "See all tool-capable models at https://openrouter.ai/models?supported_parameters=tools", "openrouter");
144
- }
145
- }
146
- return new ProviderError(`OpenRouter error: ${errorRecord?.message || "Unknown error"}`, "openrouter");
148
+ "See all tool-capable models at https://openrouter.ai/models?supported_parameters=tools",
149
+ },
150
+ ...DEFAULT_ERROR_RULES,
151
+ ];
152
+ return classifyProviderError(error, rules, "openrouter", this.modelName);
147
153
  }
148
154
  // ===========================================================================
149
155
  // Optional hooks — provider-specific quirks
@@ -1,3 +1 @@
1
- import type { OpenRouterConfig } from "../../types/index.js";
2
- export declare const getOpenRouterConfig: () => OpenRouterConfig;
3
1
  export declare const getDefaultOpenRouterModel: () => string;
@@ -1,17 +1,5 @@
1
1
  import { OpenRouterModels } from "../../constants/enums.js";
2
2
  import { getProviderModel } from "../../utils/providerConfig.js";
3
- export const getOpenRouterConfig = () => {
4
- const apiKey = process.env.OPENROUTER_API_KEY;
5
- if (!apiKey) {
6
- throw new Error("OPENROUTER_API_KEY environment variable is required. " +
7
- "Get your API key at https://openrouter.ai/keys");
8
- }
9
- return {
10
- apiKey,
11
- referer: process.env.OPENROUTER_REFERER,
12
- appName: process.env.OPENROUTER_APP_NAME,
13
- };
14
- };
15
3
  export const getDefaultOpenRouterModel = () => {
16
4
  return getProviderModel("OPENROUTER_MODEL", OpenRouterModels.CLAUDE_SONNET_4_5);
17
5
  };
@@ -166,6 +166,15 @@ export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider
166
166
  * override this to probe the server's `/models` endpoint.
167
167
  */
168
168
  validateConfiguration(): Promise<boolean>;
169
+ /**
170
+ * Shared local-runtime reachability probe: GET `${baseURL}/models` with a
171
+ * short timeout, requiring at least one model entry with a non-empty id.
172
+ * Local providers (Ollama, LM Studio, llama.cpp) call this from their own
173
+ * validateConfiguration() override instead of relying on the base class's
174
+ * "apiKey is a non-empty string" default, which can't detect an
175
+ * unreachable local server.
176
+ */
177
+ protected probeModelsEndpoint(headers?: Record<string, string>): Promise<boolean>;
169
178
  /**
170
179
  * Snapshot of the provider's resolved configuration — part of the documented
171
180
  * public provider contract (`docs/provider-integration/00-architecture.md`).
@@ -17,6 +17,7 @@
17
17
  * Nothing here imports from "ai" or "@ai-sdk/*". The base class is a
18
18
  * direct HTTP client + multi-step tool-execution loop driven by SSE.
19
19
  */
20
+ import { trace } from "@opentelemetry/api";
20
21
  import { getAvailableInputTokens, getRuntimeContextWindow, getRuntimeOutputCeiling, registerRuntimeContextWindow, } from "../constants/contextWindows.js";
21
22
  import { guardOpenAICompatConversation } from "../context/openaiCompatLoopGuard.js";
22
23
  import { isContextOverflowError, parseProviderOverflowDetails, } from "../context/errorDetection.js";
@@ -26,12 +27,14 @@ import { DEFAULT_MAX_STEPS } from "../core/constants.js";
26
27
  import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
27
28
  import { createProxyFetch } from "../proxy/proxyFetch.js";
28
29
  import { logger } from "../utils/logger.js";
30
+ import { redactUrlCredentials } from "../utils/logSanitize.js";
29
31
  import { NoOutputGeneratedError } from "../utils/generationErrors.js";
30
32
  import { buildNoOutputSentinel, stampNoOutputSpan, } from "../utils/noOutputSentinel.js";
31
33
  import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals, } from "../utils/timeout.js";
32
34
  import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
33
35
  import { resolveToolChoice } from "../utils/toolChoice.js";
34
36
  import { transformToolExecutions } from "../utils/transformationUtils.js";
37
+ import { withProviderRetry } from "../utils/providerRetry.js";
35
38
  import { resolveDeferredTool } from "../tools/toolDiscovery.js";
36
39
  import { buildAPIError, buildBody, buildToolsForOpenAI, buildWireToolNameMaps, createChunkQueue, createDeferredAnalytics, ensureJsonWordInBody, estimateWireTokens, mapNeuroLinkToolChoice, mergeUsage, messageBuilderToOpenAI, parseSSEStream, stringifyToolOutput, stripTrailingSlash, v3ResponseFormatToOpenAI, v3ToolChoiceToOpenAI, v3ToolsToOpenAI, } from "./openaiChatCompletionsClient.js";
37
40
  /**
@@ -272,6 +275,38 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
272
275
  return (typeof this.config.apiKey === "string" &&
273
276
  this.config.apiKey.trim().length > 0);
274
277
  }
278
+ /**
279
+ * Shared local-runtime reachability probe: GET `${baseURL}/models` with a
280
+ * short timeout, requiring at least one model entry with a non-empty id.
281
+ * Local providers (Ollama, LM Studio, llama.cpp) call this from their own
282
+ * validateConfiguration() override instead of relying on the base class's
283
+ * "apiKey is a non-empty string" default, which can't detect an
284
+ * unreachable local server.
285
+ */
286
+ async probeModelsEndpoint(headers = {}) {
287
+ try {
288
+ const url = `${stripTrailingSlash(this.config.baseURL)}/models`;
289
+ const proxyFetch = createProxyFetch();
290
+ const response = await proxyFetch(url, {
291
+ headers: { ...headers, "Content-Type": "application/json" },
292
+ signal: AbortSignal.timeout(5000),
293
+ });
294
+ if (!response.ok) {
295
+ return false;
296
+ }
297
+ const data = (await response
298
+ .json()
299
+ .catch(() => null));
300
+ return Boolean(data?.data?.some((m) => typeof m?.id === "string" && m.id.trim().length > 0));
301
+ }
302
+ catch (error) {
303
+ logger.debug(`[${this.constructor.name}] probeModelsEndpoint failed`, {
304
+ baseURL: redactUrlCredentials(this.config.baseURL),
305
+ error: error instanceof Error ? error.message : String(error),
306
+ });
307
+ return false;
308
+ }
309
+ }
275
310
  /**
276
311
  * Snapshot of the provider's resolved configuration — part of the documented
277
312
  * public provider contract (`docs/provider-integration/00-architecture.md`).
@@ -899,22 +934,44 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
899
934
  : {}),
900
935
  streaming: true,
901
936
  }), args.modelId));
902
- let res = await args.fetchImpl(args.url, {
903
- method: "POST",
904
- headers: {
905
- "Content-Type": "application/json",
906
- ...this.getAuthHeaders(),
907
- },
908
- body: JSON.stringify(body),
909
- ...(args.abortSignal ? { signal: args.abortSignal } : {}),
910
- });
911
- if (!res.ok) {
912
- const apiErr = await buildAPIError(args.url, body, res);
913
- // One-shot 400 retry — overflow corrector first (re-fits max_tokens
914
- // from the provider's own numbers + self-heals the window registry),
915
- // then the subclass hook (e.g. NIM strips chat_template /
916
- // reasoning_budget when a model rejects them).
917
- const retryBody = res.status === 400
937
+ // The initial fetch gets 429/5xx retry-with-backoff via the same
938
+ // primitive the non-streaming path already uses (withProviderRetry).
939
+ // `doFetch` throws the classified APIError (buildAPIError attaches
940
+ // .statusCode + .responseHeaders, which withProviderRetry's duck-typing
941
+ // reads directly) so a non-ok response is what drives the retry
942
+ // decision, not a return value.
943
+ const doFetch = async () => {
944
+ const attemptRes = await args.fetchImpl(args.url, {
945
+ method: "POST",
946
+ headers: {
947
+ "Content-Type": "application/json",
948
+ ...this.getAuthHeaders(),
949
+ },
950
+ body: JSON.stringify(body),
951
+ ...(args.abortSignal ? { signal: args.abortSignal } : {}),
952
+ });
953
+ if (!attemptRes.ok) {
954
+ throw await buildAPIError(args.url, body, attemptRes);
955
+ }
956
+ return attemptRes;
957
+ };
958
+ let res;
959
+ try {
960
+ res = await withProviderRetry(doFetch, trace.getActiveSpan() ?? undefined, `${this.providerName} stream`);
961
+ }
962
+ catch (err) {
963
+ // The one-shot 400 context-overflow fallback lives outside
964
+ // withProviderRetry (400 isn't retryable there anyway — see
965
+ // isRetryableProviderError), so it fires on the classified error's
966
+ // .statusCode. The raw Response is no longer in scope here: it was
967
+ // consumed inside doFetch's closure, either returned on success or
968
+ // discarded after buildAPIError read its body on failure.
969
+ const apiErr = err;
970
+ // Overflow corrector first (re-fits max_tokens from the provider's
971
+ // own numbers + self-heals the window registry), then the subclass
972
+ // hook (e.g. NIM strips chat_template / reasoning_budget when a model
973
+ // rejects them).
974
+ const retryBody = apiErr.statusCode === 400
918
975
  ? (this.correctBodyAfterContextOverflow(body, apiErr) ??
919
976
  this.adjustBodyAfter400(body, apiErr))
920
977
  : undefined;