@juspay/neurolink 10.12.8 → 11.0.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 (179) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/browser/neurolink.min.js +399 -401
  3. package/dist/cli/commands/setup.d.ts +16 -1
  4. package/dist/cli/commands/setup.js +141 -3
  5. package/dist/core/baseProvider.js +3 -3
  6. package/dist/core/dynamicModels.d.ts +1 -1
  7. package/dist/core/dynamicModels.js +1 -23
  8. package/dist/core/modelConfiguration.d.ts +1 -70
  9. package/dist/core/modelConfiguration.js +0 -303
  10. package/dist/core/modules/GenerationHandler.js +38 -16
  11. package/dist/core/modules/structuredOutputPolicy.d.ts +2 -2
  12. package/dist/core/modules/structuredOutputPolicy.js +2 -2
  13. package/dist/factories/providerFactory.d.ts +15 -0
  14. package/dist/factories/providerFactory.js +27 -39
  15. package/dist/factories/providerRegistry.js +2 -2
  16. package/dist/lib/core/baseProvider.js +3 -3
  17. package/dist/lib/core/dynamicModels.d.ts +1 -1
  18. package/dist/lib/core/dynamicModels.js +1 -23
  19. package/dist/lib/core/modelConfiguration.d.ts +1 -70
  20. package/dist/lib/core/modelConfiguration.js +0 -303
  21. package/dist/lib/core/modules/GenerationHandler.js +38 -16
  22. package/dist/lib/core/modules/structuredOutputPolicy.d.ts +2 -2
  23. package/dist/lib/core/modules/structuredOutputPolicy.js +2 -2
  24. package/dist/lib/factories/providerFactory.d.ts +15 -0
  25. package/dist/lib/factories/providerFactory.js +27 -39
  26. package/dist/lib/factories/providerRegistry.js +2 -2
  27. package/dist/lib/models/anthropicModels.d.ts +0 -7
  28. package/dist/lib/models/anthropicModels.js +0 -9
  29. package/dist/lib/neurolink.d.ts +15 -0
  30. package/dist/lib/neurolink.js +77 -44
  31. package/dist/lib/providers/anthropic/client.js +17 -1
  32. package/dist/lib/providers/anthropic/constants.d.ts +0 -1
  33. package/dist/lib/providers/anthropic/constants.js +0 -2
  34. package/dist/lib/providers/anthropic/index.d.ts +0 -1
  35. package/dist/lib/providers/anthropic/index.js +0 -1
  36. package/dist/lib/providers/googleAiStudio/index.d.ts +0 -1
  37. package/dist/lib/providers/googleAiStudio/index.js +0 -1
  38. package/dist/lib/providers/googleNativeGemini3/index.d.ts +0 -1
  39. package/dist/lib/providers/googleNativeGemini3/index.js +0 -1
  40. package/dist/lib/providers/googleVertex/client.d.ts +0 -51
  41. package/dist/lib/providers/googleVertex/client.js +0 -678
  42. package/dist/lib/providers/huggingFace/client.d.ts +1 -1
  43. package/dist/lib/providers/huggingFace/client.js +1 -1
  44. package/dist/lib/providers/huggingFace/index.d.ts +0 -1
  45. package/dist/lib/providers/huggingFace/index.js +0 -1
  46. package/dist/lib/providers/jina.d.ts +0 -1
  47. package/dist/lib/providers/jina.js +0 -1
  48. package/dist/lib/providers/litellm/index.d.ts +0 -2
  49. package/dist/lib/providers/litellm/index.js +0 -2
  50. package/dist/lib/providers/llamaCpp.d.ts +1 -0
  51. package/dist/lib/providers/llamaCpp.js +3 -0
  52. package/dist/lib/providers/lmStudio.js +3 -23
  53. package/dist/lib/providers/nvidiaNim/index.d.ts +0 -1
  54. package/dist/lib/providers/nvidiaNim/index.js +0 -1
  55. package/dist/lib/providers/ollama/client.js +1 -23
  56. package/dist/lib/providers/ollama/index.d.ts +0 -1
  57. package/dist/lib/providers/ollama/index.js +0 -1
  58. package/dist/lib/providers/openAI/index.d.ts +0 -2
  59. package/dist/lib/providers/openAI/index.js +0 -2
  60. package/dist/lib/providers/openRouter/utils.d.ts +0 -2
  61. package/dist/lib/providers/openRouter/utils.js +0 -12
  62. package/dist/lib/providers/openaiChatCompletionsBase.d.ts +9 -0
  63. package/dist/lib/providers/openaiChatCompletionsBase.js +33 -0
  64. package/dist/lib/providers/perplexity.d.ts +2 -1
  65. package/dist/lib/providers/perplexity.js +2 -1
  66. package/dist/lib/providers/replicate.d.ts +0 -1
  67. package/dist/lib/providers/replicate.js +22 -9
  68. package/dist/lib/providers/voyage.d.ts +0 -1
  69. package/dist/lib/providers/voyage.js +0 -1
  70. package/dist/lib/types/index.d.ts +0 -1
  71. package/dist/lib/types/index.js +0 -1
  72. package/dist/lib/types/providers.d.ts +2 -11
  73. package/dist/lib/types/utilities.d.ts +29 -0
  74. package/dist/lib/utils/json/coerce.d.ts +21 -1
  75. package/dist/lib/utils/json/coerce.js +148 -11
  76. package/dist/lib/utils/providerConfig.d.ts +0 -8
  77. package/dist/lib/utils/providerConfig.js +0 -37
  78. package/dist/lib/utils/providerUtils.js +1 -25
  79. package/dist/models/anthropicModels.d.ts +0 -7
  80. package/dist/models/anthropicModels.js +0 -9
  81. package/dist/neurolink.d.ts +15 -0
  82. package/dist/neurolink.js +77 -44
  83. package/dist/providers/anthropic/client.js +17 -1
  84. package/dist/providers/anthropic/constants.d.ts +0 -1
  85. package/dist/providers/anthropic/constants.js +0 -2
  86. package/dist/providers/anthropic/index.d.ts +0 -1
  87. package/dist/providers/anthropic/index.js +0 -1
  88. package/dist/providers/googleAiStudio/index.d.ts +0 -1
  89. package/dist/providers/googleAiStudio/index.js +0 -1
  90. package/dist/providers/googleNativeGemini3/index.d.ts +0 -1
  91. package/dist/providers/googleNativeGemini3/index.js +0 -1
  92. package/dist/providers/googleVertex/client.d.ts +0 -51
  93. package/dist/providers/googleVertex/client.js +0 -678
  94. package/dist/providers/huggingFace/client.d.ts +1 -1
  95. package/dist/providers/huggingFace/client.js +1 -1
  96. package/dist/providers/huggingFace/index.d.ts +0 -1
  97. package/dist/providers/huggingFace/index.js +0 -1
  98. package/dist/providers/jina.d.ts +0 -1
  99. package/dist/providers/jina.js +0 -1
  100. package/dist/providers/litellm/index.d.ts +0 -2
  101. package/dist/providers/litellm/index.js +0 -2
  102. package/dist/providers/llamaCpp.d.ts +1 -0
  103. package/dist/providers/llamaCpp.js +3 -0
  104. package/dist/providers/lmStudio.js +3 -23
  105. package/dist/providers/nvidiaNim/index.d.ts +0 -1
  106. package/dist/providers/nvidiaNim/index.js +0 -1
  107. package/dist/providers/ollama/client.js +1 -23
  108. package/dist/providers/ollama/index.d.ts +0 -1
  109. package/dist/providers/ollama/index.js +0 -1
  110. package/dist/providers/openAI/index.d.ts +0 -2
  111. package/dist/providers/openAI/index.js +0 -2
  112. package/dist/providers/openRouter/utils.d.ts +0 -2
  113. package/dist/providers/openRouter/utils.js +0 -12
  114. package/dist/providers/openaiChatCompletionsBase.d.ts +9 -0
  115. package/dist/providers/openaiChatCompletionsBase.js +33 -0
  116. package/dist/providers/perplexity.d.ts +2 -1
  117. package/dist/providers/perplexity.js +2 -1
  118. package/dist/providers/replicate.d.ts +0 -1
  119. package/dist/providers/replicate.js +22 -9
  120. package/dist/providers/voyage.d.ts +0 -1
  121. package/dist/providers/voyage.js +0 -1
  122. package/dist/types/index.d.ts +0 -1
  123. package/dist/types/index.js +0 -1
  124. package/dist/types/providers.d.ts +2 -11
  125. package/dist/types/utilities.d.ts +29 -0
  126. package/dist/utils/json/coerce.d.ts +21 -1
  127. package/dist/utils/json/coerce.js +148 -11
  128. package/dist/utils/providerConfig.d.ts +0 -8
  129. package/dist/utils/providerConfig.js +0 -37
  130. package/dist/utils/providerUtils.js +1 -25
  131. package/package.json +8 -6
  132. package/dist/lib/providers/anthropic/utils.d.ts +0 -7
  133. package/dist/lib/providers/anthropic/utils.js +0 -150
  134. package/dist/lib/providers/googleAiStudio/utils.d.ts +0 -2
  135. package/dist/lib/providers/googleAiStudio/utils.js +0 -19
  136. package/dist/lib/providers/googleNativeGemini3/constants.d.ts +0 -1
  137. package/dist/lib/providers/googleNativeGemini3/constants.js +0 -2
  138. package/dist/lib/providers/huggingFace/utils.d.ts +0 -2
  139. package/dist/lib/providers/huggingFace/utils.js +0 -8
  140. package/dist/lib/providers/index.d.ts +0 -33
  141. package/dist/lib/providers/index.js +0 -34
  142. package/dist/lib/providers/litellm/constants.d.ts +0 -1
  143. package/dist/lib/providers/litellm/constants.js +0 -3
  144. package/dist/lib/providers/litellm/utils.d.ts +0 -5
  145. package/dist/lib/providers/litellm/utils.js +0 -11
  146. package/dist/lib/providers/nvidiaNim/utils.d.ts +0 -9
  147. package/dist/lib/providers/nvidiaNim/utils.js +0 -101
  148. package/dist/lib/providers/ollama/utils.d.ts +0 -8
  149. package/dist/lib/providers/ollama/utils.js +0 -43
  150. package/dist/lib/providers/openAI/constants.d.ts +0 -1
  151. package/dist/lib/providers/openAI/constants.js +0 -3
  152. package/dist/lib/providers/openAI/utils.d.ts +0 -2
  153. package/dist/lib/providers/openAI/utils.js +0 -8
  154. package/dist/lib/types/universalProviderOptions.d.ts +0 -89
  155. package/dist/lib/types/universalProviderOptions.js +0 -55
  156. package/dist/providers/anthropic/utils.d.ts +0 -7
  157. package/dist/providers/anthropic/utils.js +0 -149
  158. package/dist/providers/googleAiStudio/utils.d.ts +0 -2
  159. package/dist/providers/googleAiStudio/utils.js +0 -18
  160. package/dist/providers/googleNativeGemini3/constants.d.ts +0 -1
  161. package/dist/providers/googleNativeGemini3/constants.js +0 -1
  162. package/dist/providers/huggingFace/utils.d.ts +0 -2
  163. package/dist/providers/huggingFace/utils.js +0 -7
  164. package/dist/providers/index.d.ts +0 -33
  165. package/dist/providers/index.js +0 -33
  166. package/dist/providers/litellm/constants.d.ts +0 -1
  167. package/dist/providers/litellm/constants.js +0 -2
  168. package/dist/providers/litellm/utils.d.ts +0 -5
  169. package/dist/providers/litellm/utils.js +0 -10
  170. package/dist/providers/nvidiaNim/utils.d.ts +0 -9
  171. package/dist/providers/nvidiaNim/utils.js +0 -100
  172. package/dist/providers/ollama/utils.d.ts +0 -8
  173. package/dist/providers/ollama/utils.js +0 -42
  174. package/dist/providers/openAI/constants.d.ts +0 -1
  175. package/dist/providers/openAI/constants.js +0 -2
  176. package/dist/providers/openAI/utils.d.ts +0 -2
  177. package/dist/providers/openAI/utils.js +0 -7
  178. package/dist/types/universalProviderOptions.d.ts +0 -89
  179. package/dist/types/universalProviderOptions.js +0 -54
@@ -23,6 +23,48 @@ import { nextBalancedJsonSpan } from "./extract.js";
23
23
  function hasSafeParse(schema) {
24
24
  return typeof schema.safeParse === "function";
25
25
  }
26
+ /**
27
+ * Does `value` satisfy `schema`? A schema we cannot validate with (absent, or
28
+ * without a Zod-style `safeParse`) accepts everything — callers use this as a
29
+ * gate, not as proof, so an unknown schema must never block a value.
30
+ *
31
+ * Exists so the consumers of coerceJsonToSchema can refuse to publish a
32
+ * recovered value as `structuredData` when the caller's schema rejects it —
33
+ * e.g. a raw *string* under an object schema, which is the shape a truncated
34
+ * response degrades to.
35
+ */
36
+ export function schemaAccepts(schema, value) {
37
+ if (!schema || !hasSafeParse(schema)) {
38
+ return true;
39
+ }
40
+ return schema.safeParse(value).success;
41
+ }
42
+ /**
43
+ * Recover a JSON *scalar* root (string/number/boolean) from model text. The
44
+ * object/array case is handled by `coerceJsonToSchema`; this covers the
45
+ * residual scalar root after that path returns null. Encapsulates the JSON
46
+ * parsing, the empty-string normalization, and the `schemaAccepts` gate so the
47
+ * same policy cannot drift between consumers (`neurolink.recoverStructuredData`
48
+ * and `GenerationHandler.coerceTextMode`).
49
+ */
50
+ export function recoverScalarRoot(text, schema) {
51
+ try {
52
+ const scalar = JSON.parse(text);
53
+ if (scalar === "") {
54
+ return { kind: "empty" };
55
+ }
56
+ if (scalar === null || scalar === undefined) {
57
+ return { kind: "nullish" };
58
+ }
59
+ if (schemaAccepts(schema, scalar)) {
60
+ return { kind: "accepted", value: scalar };
61
+ }
62
+ return { kind: "rejected", value: scalar };
63
+ }
64
+ catch {
65
+ return { kind: "not-json" };
66
+ }
67
+ }
26
68
  /**
27
69
  * Parse `candidate` as JSON, repairing common escaping mistakes on failure.
28
70
  * Returns the parsed value plus whether jsonrepair had to alter the text.
@@ -51,6 +93,34 @@ function parseOrRepair(candidate) {
51
93
  }
52
94
  /** Bounds the recursive nested-string unwrap against pathological inputs. */
53
95
  const MAX_NESTED_UNWRAP_DEPTH = 6;
96
+ /** Bounds the structural back-off when salvaging a truncated root object. */
97
+ const MAX_SALVAGE_TRIMS = 8;
98
+ /**
99
+ * Last-resort recovery for output cut off mid-JSON: walk the unclosed root span
100
+ * backwards to successively earlier structural boundaries (`,` / `}` / `]`) and
101
+ * re-attempt parse+repair at each one. Truncation can land somewhere jsonrepair
102
+ * cannot close on its own (inside an escape, a half-written key, a dangling
103
+ * separator); dropping back to the last completed field gives it a repairable
104
+ * prefix. Returns a PARTIAL object — the fields the model did finish — so a
105
+ * truncated response degrades to an incomplete object rather than to raw text.
106
+ */
107
+ function salvageTruncatedRoot(span) {
108
+ let candidate = span;
109
+ for (let i = 0; i < MAX_SALVAGE_TRIMS && candidate.length > 1; i++) {
110
+ const cut = Math.max(candidate.lastIndexOf(","), candidate.lastIndexOf("}"), candidate.lastIndexOf("]"));
111
+ if (cut <= 0) {
112
+ break;
113
+ }
114
+ candidate = candidate.slice(0, cut);
115
+ const outcome = parseOrRepair(candidate);
116
+ if (outcome !== undefined &&
117
+ outcome.value !== null &&
118
+ typeof outcome.value === "object") {
119
+ return outcome.value;
120
+ }
121
+ }
122
+ return undefined;
123
+ }
54
124
  /**
55
125
  * Recursively replace any string-valued field whose content is itself a JSON
56
126
  * object/array with the parsed value. Models sometimes double-encode a NESTED
@@ -135,30 +205,54 @@ export function coerceJsonToSchema(text, schema) {
135
205
  // 3. first "{" or "[" to end of text (TRUNCATED output —
136
206
  // finishReason=length — where the closing bracket was cut off;
137
207
  // jsonrepair closes it)
138
- // `truncated` marks the first-open-to-end candidate: it is only reachable
139
- // when no balanced span and no first-to-last span matched, i.e. there was no
140
- // closing bracket at all the signature of token-truncated output.
208
+ // `truncated` marks a candidate that could only come from output cut short:
209
+ // the first-open-to-end span, and when the ROOT bracket never closes
210
+ // every other candidate too, since all of them are then partial views of an
211
+ // unfinished document.
212
+ //
213
+ // `rootAligned` marks candidates that start at the document's first opening
214
+ // bracket, i.e. at the real root. Candidates are ordered root-aligned FIRST.
215
+ // On truncated output the root brace never balances, so the balanced scan
216
+ // walks on and matches brackets that live INSIDE a string value (`[step 1]`
217
+ // in a shell script, say) — a syntactically fine but semantically bogus
218
+ // array. Preferring the root keeps the partial real object ahead of that.
141
219
  const candidates = [];
220
+ const openIndexes = [text.indexOf("{"), text.indexOf("[")].filter((i) => i >= 0);
221
+ const firstOpen = openIndexes.length > 0 ? Math.min(...openIndexes) : -1;
222
+ let rootClosed = false;
142
223
  let searchFrom = 0;
143
224
  for (;;) {
144
225
  const found = nextBalancedJsonSpan(text, searchFrom);
145
226
  if (!found) {
146
227
  break;
147
228
  }
148
- candidates.push({ text: found.span, truncated: false });
229
+ const start = found.end - found.span.length;
230
+ if (start === firstOpen) {
231
+ rootClosed = true;
232
+ }
233
+ candidates.push({
234
+ text: found.span,
235
+ truncated: false,
236
+ rootAligned: start === firstOpen,
237
+ });
149
238
  searchFrom = found.end;
150
239
  }
151
- const openIndexes = [text.indexOf("{"), text.indexOf("[")].filter((i) => i >= 0);
152
- const firstOpen = openIndexes.length > 0 ? Math.min(...openIndexes) : -1;
240
+ // No balanced span starts at the root bracket → the document is unclosed.
241
+ const rootUnclosed = firstOpen >= 0 && !rootClosed;
153
242
  const lastClose = Math.max(text.lastIndexOf("}"), text.lastIndexOf("]"));
154
243
  if (firstOpen >= 0 && lastClose > firstOpen) {
155
244
  candidates.push({
156
245
  text: text.slice(firstOpen, lastClose + 1),
157
- truncated: false,
246
+ truncated: rootUnclosed,
247
+ rootAligned: true,
158
248
  });
159
249
  }
160
250
  if (firstOpen >= 0) {
161
- candidates.push({ text: text.slice(firstOpen), truncated: true });
251
+ candidates.push({
252
+ text: text.slice(firstOpen),
253
+ truncated: true,
254
+ rootAligned: true,
255
+ });
162
256
  }
163
257
  // JSON-string-literal wrapper: some providers double-encode and return the
164
258
  // object as a JSON *string* (e.g. `"{\"k\":1}"`). Unwrap one layer and add
@@ -174,7 +268,11 @@ export function coerceJsonToSchema(text, schema) {
174
268
  if (!innerSpan) {
175
269
  break;
176
270
  }
177
- candidates.push({ text: innerSpan.span, truncated: false });
271
+ candidates.push({
272
+ text: innerSpan.span,
273
+ truncated: false,
274
+ rootAligned: false,
275
+ });
178
276
  innerFrom = innerSpan.end;
179
277
  }
180
278
  }
@@ -183,11 +281,20 @@ export function coerceJsonToSchema(text, schema) {
183
281
  // not a string literal — ignore
184
282
  }
185
283
  }
284
+ // Stable partition: root-aligned candidates first. Only the ORDER changes —
285
+ // no candidate is dropped — so the multi-object "prefer the most complete
286
+ // schema-valid candidate" selection below is unaffected (it is order
287
+ // independent), while the unschema'd first-parseable-wins path and the
288
+ // `firstValid` fallback now favour the document's real root.
289
+ const ordered = [
290
+ ...candidates.filter((c) => c.rootAligned),
291
+ ...candidates.filter((c) => !c.rootAligned),
292
+ ];
186
293
  let firstValid;
187
294
  const schemaValid = [];
188
295
  const hasSchema = !!(schema && hasSafeParse(schema));
189
296
  const seen = new Set();
190
- for (const candidate of candidates) {
297
+ for (const candidate of ordered) {
191
298
  if (seen.has(candidate.text)) {
192
299
  continue;
193
300
  }
@@ -202,6 +309,7 @@ export function coerceJsonToSchema(text, schema) {
202
309
  value: outcome.value,
203
310
  repaired: outcome.repaired,
204
311
  truncated: candidate.truncated,
312
+ rootAligned: candidate.rootAligned,
205
313
  };
206
314
  if (firstValid === undefined) {
207
315
  firstValid = record;
@@ -229,6 +337,7 @@ export function coerceJsonToSchema(text, schema) {
229
337
  value: unwrapped.value,
230
338
  repaired: true,
231
339
  truncated: candidate.truncated,
340
+ rootAligned: candidate.rootAligned,
232
341
  });
233
342
  }
234
343
  else if (
@@ -247,6 +356,7 @@ export function coerceJsonToSchema(text, schema) {
247
356
  value: outcome.value[0],
248
357
  repaired: true,
249
358
  truncated: candidate.truncated,
359
+ rootAligned: candidate.rootAligned,
250
360
  });
251
361
  }
252
362
  }
@@ -261,7 +371,34 @@ export function coerceJsonToSchema(text, schema) {
261
371
  ? cur
262
372
  : best)
263
373
  : undefined;
264
- const chosen = schemaMatch ?? firstValid;
374
+ let chosen = schemaMatch ?? firstValid;
375
+ // Salvage the root when the document is unclosed AND nothing trustworthy came
376
+ // out of it: either nothing parsed at all, or the only parseable candidate is
377
+ // a bracket pair scraped from INSIDE a string value (not root-aligned, not
378
+ // schema-valid) — the `["step 1"]`-from-a-shell-script case. Backing off to
379
+ // the last completed field yields a PARTIAL OBJECT flagged `truncated`,
380
+ // instead of a bogus value or raw text degrading to a string.
381
+ if (rootUnclosed &&
382
+ schemaMatch === undefined &&
383
+ (chosen === undefined || !chosen.rootAligned)) {
384
+ const salvaged = salvageTruncatedRoot(text.slice(firstOpen));
385
+ if (salvaged !== undefined) {
386
+ logger.debug("[coerceJsonToSchema] salvaged a partial truncated object", {
387
+ textLength: text.length,
388
+ });
389
+ chosen = {
390
+ value: salvaged,
391
+ repaired: true,
392
+ truncated: true,
393
+ rootAligned: true,
394
+ };
395
+ }
396
+ else if (chosen !== undefined) {
397
+ // Nothing salvageable, but we still know the output was truncated —
398
+ // never report a candidate scraped from a truncated document as complete.
399
+ chosen = { ...chosen, truncated: true };
400
+ }
401
+ }
265
402
  if (chosen === undefined) {
266
403
  return null;
267
404
  }
@@ -115,14 +115,6 @@ export declare function createDeepSeekConfig(): ProviderConfigOptions;
115
115
  * Creates NVIDIA NIM provider configuration
116
116
  */
117
117
  export declare function createNvidiaNimConfig(): ProviderConfigOptions;
118
- /**
119
- * Creates LM Studio provider configuration (local server)
120
- */
121
- export declare function createLmStudioConfig(): ProviderConfigOptions;
122
- /**
123
- * Creates llama.cpp provider configuration (local server)
124
- */
125
- export declare function createLlamaCppConfig(): ProviderConfigOptions;
126
118
  /**
127
119
  * Creates xAI Grok provider configuration.
128
120
  */
@@ -411,43 +411,6 @@ export function createNvidiaNimConfig() {
411
411
  ],
412
412
  };
413
413
  }
414
- /**
415
- * Creates LM Studio provider configuration (local server)
416
- */
417
- export function createLmStudioConfig() {
418
- return {
419
- providerName: "LM Studio",
420
- envVarName: "LM_STUDIO_BASE_URL",
421
- setupUrl: "https://lmstudio.ai/",
422
- description: "LM Studio server URL",
423
- instructions: [
424
- "1. Install LM Studio: https://lmstudio.ai/",
425
- "2. Open LM Studio and download a model (e.g. Llama 3.2 3B Instruct)",
426
- '3. Click "Local Server" → Start Server',
427
- "4. Default URL is http://localhost:1234/v1 (override via LM_STUDIO_BASE_URL)",
428
- ],
429
- // Base URL is optional — defaults to http://localhost:1234/v1 if unset.
430
- optional: true,
431
- };
432
- }
433
- /**
434
- * Creates llama.cpp provider configuration (local server)
435
- */
436
- export function createLlamaCppConfig() {
437
- return {
438
- providerName: "llama.cpp",
439
- envVarName: "LLAMACPP_BASE_URL",
440
- setupUrl: "https://github.com/ggerganov/llama.cpp",
441
- description: "llama.cpp server URL",
442
- instructions: [
443
- "1. Build llama.cpp: https://github.com/ggerganov/llama.cpp#build",
444
- "2. Run: ./llama-server -m model.gguf --port 8080",
445
- "3. Default URL is http://localhost:8080/v1 (override via LLAMACPP_BASE_URL)",
446
- ],
447
- // Base URL is optional — defaults to http://localhost:8080/v1 if unset.
448
- optional: true,
449
- };
450
- }
451
414
  /**
452
415
  * Creates xAI Grok provider configuration.
453
416
  */
@@ -20,19 +20,6 @@ export async function getBestProvider(requestedProvider) {
20
20
  // For explicit provider requests, ALWAYS honor the request
21
21
  // Never override explicit provider selection with health-based fallbacks
22
22
  logger.debug(`[getBestProvider] Using explicitly requested provider: ${requestedProvider}`);
23
- // Optional health check for logging purposes only
24
- try {
25
- const health = await ProviderHealthChecker.checkProviderHealth(requestedProvider, { includeConnectivityTest: false, cacheResults: true });
26
- if (health.isHealthy) {
27
- logger.debug(`[getBestProvider] Explicitly requested provider ${requestedProvider} is healthy`);
28
- }
29
- else {
30
- logger.warn(`[getBestProvider] Explicitly requested provider ${requestedProvider} may have issues, but using anyway`, { error: health.error });
31
- }
32
- }
33
- catch (error) {
34
- logger.warn(`[getBestProvider] Health check failed for explicitly requested provider ${requestedProvider}, using anyway`, { error: error instanceof Error ? error.message : String(error) });
35
- }
36
23
  // ALWAYS return the explicitly requested provider
37
24
  return requestedProvider;
38
25
  }
@@ -402,18 +389,7 @@ export function hasProviderEnvVars(provider) {
402
389
  * @returns Array of available provider names
403
390
  */
404
391
  export function getAvailableProviders() {
405
- return [
406
- "bedrock",
407
- "vertex",
408
- "openai",
409
- "anthropic",
410
- "azure",
411
- "google-ai",
412
- "litellm",
413
- "huggingface",
414
- "ollama",
415
- "mistral",
416
- ];
392
+ return Object.values(AIProviderName).filter((name) => name !== AIProviderName.AUTO);
417
393
  }
418
394
  /**
419
395
  * Validate provider name
@@ -242,13 +242,6 @@ export declare function getContextWindow(model: string): number;
242
242
  * @returns The max output tokens, or 0 if model not found
243
243
  */
244
244
  export declare function getMaxOutputTokens(model: string): number;
245
- /**
246
- * Check if a model supports vision/image input
247
- *
248
- * @param model - The model ID
249
- * @returns true if the model supports vision
250
- */
251
- export declare function supportsVision(model: string): boolean;
252
245
  /**
253
246
  * Check if a model supports extended thinking
254
247
  *
@@ -531,15 +531,6 @@ export function getContextWindow(model) {
531
531
  export function getMaxOutputTokens(model) {
532
532
  return MODEL_METADATA[model]?.maxOutputTokens ?? 0;
533
533
  }
534
- /**
535
- * Check if a model supports vision/image input
536
- *
537
- * @param model - The model ID
538
- * @returns true if the model supports vision
539
- */
540
- export function supportsVision(model) {
541
- return MODEL_METADATA[model]?.supportsVision ?? false;
542
- }
543
534
  /**
544
535
  * Check if a model supports extended thinking
545
536
  *
@@ -775,6 +775,21 @@ export declare class NeuroLink {
775
775
  private applyClassifierRouting;
776
776
  private prepareGenerateAugmentations;
777
777
  private buildGenerateTextOptions;
778
+ /**
779
+ * Provider-agnostic JSON recovery for schema requests. Structured-output
780
+ * enforcement makes valid JSON the overwhelming case; for every other
781
+ * provider path — including generate() overrides (Vertex, Anthropic,
782
+ * Bedrock, Google AI Studio) — object/array roots are recovered here via
783
+ * balanced-scan + jsonrepair and scalar JSON roots via plain JSON.parse,
784
+ * with the parsed value exposed as `structuredData`. If nothing JSON-shaped
785
+ * is recoverable (pure prose), the raw text is returned, `structuredData`
786
+ * stays undefined, and a WARN makes the case observable.
787
+ *
788
+ * Mutates `textResult` in place, and must run BEFORE the end-of-generation
789
+ * emits so event consumers see the same content/structuredData the caller
790
+ * receives.
791
+ */
792
+ private recoverStructuredData;
778
793
  private finalizeGenerateRequestResult;
779
794
  private emitGenerateErrorEvent;
780
795
  /**
package/dist/neurolink.js CHANGED
@@ -74,7 +74,7 @@ import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableErr
74
74
  import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "./utils/lifecycleCallbacks.js";
75
75
  import { resolveLifecycleTimeoutMs } from "./utils/lifecycleTimeout.js";
76
76
  import { cloneOptionsForCallIsolation } from "./utils/cloneOptions.js";
77
- import { coerceJsonToSchema } from "./utils/json/coerce.js";
77
+ import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "./utils/json/coerce.js";
78
78
  // Factory processing imports
79
79
  import { createCleanStreamOptions, enhanceTextGenerationOptions, processFactoryOptions, processStreamingFactoryOptions, validateFactoryConfig, } from "./utils/factoryProcessing.js";
80
80
  import { logger, mcpLogger } from "./utils/logger.js";
@@ -4058,52 +4058,85 @@ Current user's request: ${currentInput}`;
4058
4058
  }
4059
4059
  return textOptions;
4060
4060
  }
4061
- finalizeGenerateRequestResult(params) {
4062
- const { generateSpan, options, textOptions, textResult, factoryResult, originalPrompt, startTime, } = params;
4063
- // Provider-agnostic JSON coercion for schema requests. Structured-output
4064
- // enforcement makes valid JSON the overwhelming case; for every other
4065
- // provider pathincluding generate() overrides (Vertex, Anthropic,
4066
- // Bedrock, Google AI Studio) object/array roots are recovered here via
4067
- // balanced-scan + jsonrepair and scalar JSON roots via plain JSON.parse,
4068
- // with the parsed value exposed as `structuredData`. If nothing
4069
- // JSON-shaped is recoverable (pure prose), the raw text is returned,
4070
- // `structuredData` stays undefined, and a WARN makes the case observable.
4071
- // Runs BEFORE the end-of-generation emits below so event consumers see
4072
- // the same coerced content/structuredData the caller receives.
4073
- if (textOptions.schema &&
4074
- textResult.structuredData === undefined &&
4075
- typeof textResult.content === "string") {
4076
- const coerced = coerceJsonToSchema(textResult.content, textOptions.schema);
4077
- if (coerced) {
4078
- textResult.content = coerced.content;
4079
- textResult.structuredData = coerced.structuredData;
4080
- if (coerced.repaired) {
4081
- textResult.jsonRepaired = true;
4082
- }
4083
- if (coerced.truncated) {
4084
- textResult.jsonTruncated = true;
4085
- }
4061
+ /**
4062
+ * Provider-agnostic JSON recovery for schema requests. Structured-output
4063
+ * enforcement makes valid JSON the overwhelming case; for every other
4064
+ * provider path including generate() overrides (Vertex, Anthropic,
4065
+ * Bedrock, Google AI Studio) object/array roots are recovered here via
4066
+ * balanced-scan + jsonrepair and scalar JSON roots via plain JSON.parse,
4067
+ * with the parsed value exposed as `structuredData`. If nothing JSON-shaped
4068
+ * is recoverable (pure prose), the raw text is returned, `structuredData`
4069
+ * stays undefined, and a WARN makes the case observable.
4070
+ *
4071
+ * Mutates `textResult` in place, and must run BEFORE the end-of-generation
4072
+ * emits so event consumers see the same content/structuredData the caller
4073
+ * receives.
4074
+ */
4075
+ recoverStructuredData(textResult, schema) {
4076
+ // A provider path that produced its own `structuredData` normally owns it.
4077
+ // The one exception is a STRING the caller's schema rejects: that is the
4078
+ // raw completion leaking through as structured output (the shape a
4079
+ // truncated response degrades to), so re-run recovery over the text rather
4080
+ // than handing back a value the declared schema forbids.
4081
+ const structuredIsRejectedString = typeof textResult.structuredData === "string" &&
4082
+ !schemaAccepts(schema, textResult.structuredData);
4083
+ if (!schema ||
4084
+ (textResult.structuredData !== undefined &&
4085
+ !structuredIsRejectedString) ||
4086
+ typeof textResult.content !== "string") {
4087
+ return;
4088
+ }
4089
+ if (structuredIsRejectedString) {
4090
+ textResult.structuredData = undefined;
4091
+ }
4092
+ const coerced = coerceJsonToSchema(textResult.content, schema);
4093
+ if (coerced) {
4094
+ textResult.content = coerced.content;
4095
+ textResult.structuredData = coerced.structuredData;
4096
+ if (coerced.repaired) {
4097
+ textResult.jsonRepaired = true;
4086
4098
  }
4087
- else {
4088
- try {
4089
- const scalar = JSON.parse(textResult.content);
4090
- if (scalar === "") {
4091
- // A JSON-encoded empty string is an EMPTY completion, not a
4092
- // recovered scalar — normalize to a true empty so callers'
4093
- // empty-response handling fires instead of a literal '""'
4094
- // reaching the user. `structuredData` stays undefined.
4095
- textResult.content = "";
4096
- logger.warn("[NeuroLink] schema requested but the model returned an empty JSON string; normalizing to empty content", { provider: textResult.provider, model: textResult.model });
4097
- }
4098
- else if (scalar !== null && scalar !== undefined) {
4099
- textResult.structuredData = scalar;
4100
- }
4101
- }
4102
- catch {
4103
- logger.warn("[NeuroLink] schema requested but no JSON could be recovered from model output; returning raw text", { provider: textResult.provider, model: textResult.model });
4104
- }
4099
+ if (coerced.truncated) {
4100
+ textResult.jsonTruncated = true;
4105
4101
  }
4102
+ return;
4106
4103
  }
4104
+ const scalar = recoverScalarRoot(textResult.content, schema);
4105
+ switch (scalar.kind) {
4106
+ case "empty":
4107
+ // A JSON-encoded empty string is an EMPTY completion, not a recovered
4108
+ // scalar — normalize to a true empty so callers' empty-response
4109
+ // handling fires instead of a literal '""' reaching the user.
4110
+ // `structuredData` stays undefined.
4111
+ textResult.content = "";
4112
+ logger.warn("[NeuroLink] schema requested but the model returned an empty JSON string; normalizing to empty content", { provider: textResult.provider, model: textResult.model });
4113
+ break;
4114
+ case "accepted":
4115
+ // Only publish a scalar root the caller's schema actually accepts.
4116
+ // Under an OBJECT schema a recovered string is the raw completion in
4117
+ // disguise — the shape a truncated response degrades to — and exposing
4118
+ // it hands the caller a `structuredData` that violates the schema they
4119
+ // passed.
4120
+ textResult.structuredData = scalar.value;
4121
+ break;
4122
+ case "rejected":
4123
+ logger.warn("[NeuroLink] recovered a JSON scalar the requested schema rejects; leaving structuredData unset", {
4124
+ provider: textResult.provider,
4125
+ model: textResult.model,
4126
+ scalarType: typeof scalar.value,
4127
+ });
4128
+ break;
4129
+ case "nullish":
4130
+ // JSON null/undefined — no structured value to publish.
4131
+ break;
4132
+ case "not-json":
4133
+ logger.warn("[NeuroLink] schema requested but no JSON could be recovered from model output; returning raw text", { provider: textResult.provider, model: textResult.model });
4134
+ break;
4135
+ }
4136
+ }
4137
+ finalizeGenerateRequestResult(params) {
4138
+ const { generateSpan, options, textOptions, textResult, factoryResult, originalPrompt, startTime, } = params;
4139
+ this.recoverStructuredData(textResult, textOptions.schema);
4107
4140
  // Surface truncation when a schema was requested: either the provider
4108
4141
  // reported finishReason="length" or the recovered JSON came from an
4109
4142
  // unclosed span. Either way `structuredData` may be incomplete — warn at
@@ -1206,6 +1206,9 @@ export class AnthropicProvider extends BaseProvider {
1206
1206
  }
1207
1207
  const content = [];
1208
1208
  let finalResultText;
1209
+ // Text emitted in forced-json mode, kept only as a fallback (see below).
1210
+ const jsonModeText = [];
1211
+ let jsonToolAnswered = false;
1209
1212
  for (const block of response.content) {
1210
1213
  if (block.type === "thinking") {
1211
1214
  content.push({ type: "reasoning", text: block.thinking });
@@ -1213,13 +1216,17 @@ export class AnthropicProvider extends BaseProvider {
1213
1216
  else if (block.type === "text") {
1214
1217
  // In forced-json mode the payload arrives via the tool input, not
1215
1218
  // text — pass text through only in normal mode.
1216
- if (!jsonTool) {
1219
+ if (jsonTool) {
1220
+ jsonModeText.push(block.text);
1221
+ }
1222
+ else {
1217
1223
  content.push({ type: "text", text: block.text });
1218
1224
  }
1219
1225
  }
1220
1226
  else if (block.type === "tool_use") {
1221
1227
  if (jsonTool && block.name === jsonTool) {
1222
1228
  // Unwrap the synthetic tool call back into text JSON.
1229
+ jsonToolAnswered = true;
1223
1230
  content.push({
1224
1231
  type: "text",
1225
1232
  text: stringifyToolInput(block.input),
@@ -1241,6 +1248,15 @@ export class AnthropicProvider extends BaseProvider {
1241
1248
  }
1242
1249
  }
1243
1250
  }
1251
+ // Forced-json mode normally drops text blocks because the payload rides
1252
+ // in the synthetic tool's input. But when the response is cut short
1253
+ // (stop_reason "max_tokens") the tool call can be missing entirely, and
1254
+ // dropping the text would leave an EMPTY completion with nothing for
1255
+ // coerceJsonToSchema to recover. Fall back to the text so a partial
1256
+ // object can still be salvaged and flagged truncated.
1257
+ if (jsonTool && !jsonToolAnswered && jsonModeText.length > 0) {
1258
+ content.push({ type: "text", text: jsonModeText.join("") });
1259
+ }
1244
1260
  // final_result is terminal — parity with the native Claude-on-Vertex
1245
1261
  // and Gemini loops, which break out of the tool loop the moment it
1246
1262
  // arrives. Reasoning blocks are kept; any prose preamble and any tool
@@ -1,4 +1,3 @@
1
1
  export declare const ANTHROPIC_BETA_HEADERS: {
2
2
  "anthropic-beta": string;
3
3
  };
4
- export declare const streamTracer: import("@opentelemetry/api").Tracer;
@@ -1,8 +1,6 @@
1
- import { trace } from "@opentelemetry/api";
2
1
  export const ANTHROPIC_BETA_HEADERS = {
3
2
  "anthropic-beta": [
4
3
  "claude-code-20250219",
5
4
  "fine-grained-tool-streaming-2025-05-14",
6
5
  ].join(","),
7
6
  };
8
- export const streamTracer = trace.getTracer("neurolink.provider.anthropic");
@@ -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,2 +1 @@
1
1
  export * from "./client.js";
2
- export * from "./utils.js";
@@ -1,2 +1 @@
1
1
  export * from "./client.js";
2
- export * from "./utils.js";
@@ -1,2 +1 @@
1
- export * from "./constants.js";
2
1
  export * from "./utils.js";
@@ -1,2 +1 @@
1
- export * from "./constants.js";
2
1
  export * from "./utils.js";