@juspay/neurolink 9.70.0 → 9.70.2

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 +12 -0
  2. package/dist/browser/neurolink.min.js +355 -347
  3. package/dist/core/modules/GenerationHandler.js +75 -23
  4. package/dist/core/modules/structuredOutputPolicy.d.ts +28 -0
  5. package/dist/core/modules/structuredOutputPolicy.js +50 -0
  6. package/dist/lib/core/modules/GenerationHandler.js +75 -23
  7. package/dist/lib/core/modules/structuredOutputPolicy.d.ts +28 -0
  8. package/dist/lib/core/modules/structuredOutputPolicy.js +51 -0
  9. package/dist/lib/neurolink.js +58 -0
  10. package/dist/lib/providers/anthropic.js +34 -7
  11. package/dist/lib/providers/googleVertex.js +32 -9
  12. package/dist/lib/types/generate.d.ts +47 -19
  13. package/dist/lib/types/utilities.d.ts +16 -0
  14. package/dist/lib/utils/json/coerce.d.ts +10 -0
  15. package/dist/lib/utils/json/coerce.js +141 -0
  16. package/dist/lib/utils/json/extract.d.ts +10 -0
  17. package/dist/lib/utils/json/extract.js +61 -11
  18. package/dist/lib/utils/modelDetection.d.ts +17 -0
  19. package/dist/lib/utils/modelDetection.js +23 -0
  20. package/dist/lib/utils/tokenLimits.d.ts +20 -0
  21. package/dist/lib/utils/tokenLimits.js +55 -0
  22. package/dist/neurolink.js +58 -0
  23. package/dist/providers/anthropic.js +34 -7
  24. package/dist/providers/googleVertex.js +32 -9
  25. package/dist/types/generate.d.ts +47 -19
  26. package/dist/types/utilities.d.ts +16 -0
  27. package/dist/utils/json/coerce.d.ts +10 -0
  28. package/dist/utils/json/coerce.js +140 -0
  29. package/dist/utils/json/extract.d.ts +10 -0
  30. package/dist/utils/json/extract.js +61 -11
  31. package/dist/utils/modelDetection.d.ts +17 -0
  32. package/dist/utils/modelDetection.js +23 -0
  33. package/dist/utils/tokenLimits.d.ts +20 -0
  34. package/dist/utils/tokenLimits.js +55 -0
  35. package/package.json +4 -1
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Coerce arbitrary model text into canonical, syntactically-valid JSON.
3
+ *
4
+ * Used on the text-mode path (providers/models that could not use AI-SDK
5
+ * structured output, e.g. real Gemini + tools). The model hand-writes JSON and
6
+ * frequently mis-escapes the content field (bare newline, unescaped quote,
7
+ * invalid escape like \d). A balanced-brace scan finds the object span; if
8
+ * JSON.parse rejects it, jsonrepair fixes common escaping mistakes; the result
9
+ * is re-serialised with JSON.stringify so downstream consumers always receive
10
+ * valid JSON.
11
+ *
12
+ * NOTE: jsonrepair is a heuristic. On content where a lone backslash is
13
+ * meaningful (regex/script/Windows path) it may drop the backslash, producing
14
+ * valid-but-semantically-altered content. This only affects the residual
15
+ * text-mode path — the primary Vertex+Claude path uses experimental_output and
16
+ * never reaches here. When jsonrepair changes the input we log at debug level
17
+ * so the event is observable.
18
+ */
19
+ import { jsonrepair } from "jsonrepair";
20
+ import { logger } from "../logger.js";
21
+ import { nextBalancedJsonSpan } from "./extract.js";
22
+ /** True when the schema exposes a Zod-style `safeParse` we can validate with. */
23
+ function hasSafeParse(schema) {
24
+ return typeof schema.safeParse === "function";
25
+ }
26
+ /**
27
+ * Parse `candidate` as JSON, repairing common escaping mistakes on failure.
28
+ * Returns the parsed value plus whether jsonrepair had to alter the text.
29
+ */
30
+ function parseOrRepair(candidate) {
31
+ try {
32
+ return { value: JSON.parse(candidate), repaired: false };
33
+ }
34
+ catch {
35
+ // fall through to repair
36
+ }
37
+ try {
38
+ const repaired = jsonrepair(candidate);
39
+ const value = JSON.parse(repaired);
40
+ if (repaired !== candidate && logger.shouldLog("debug")) {
41
+ logger.debug("[coerceJsonToSchema] jsonrepair altered model output", {
42
+ originalLength: candidate.length,
43
+ repairedLength: repaired.length,
44
+ });
45
+ }
46
+ return { value, repaired: repaired !== candidate };
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ }
52
+ /**
53
+ * Try to produce canonical JSON from `text`. Returns null when no JSON object
54
+ * could be recovered (caller should then keep the raw text).
55
+ *
56
+ * When `schema` is a Zod schema, candidates that satisfy it are preferred; a
57
+ * syntactically-valid-but-schema-failing object is still returned (we guarantee
58
+ * JSON *validity*, leaving schema/content checks to the caller's own pipeline).
59
+ */
60
+ export function coerceJsonToSchema(text, schema) {
61
+ if (typeof text !== "string" || text.trim().length === 0) {
62
+ return null;
63
+ }
64
+ // Ordered candidate substrings, best-formed first:
65
+ // 1. every balanced object/array span (clean, common case)
66
+ // 2. first "{" or "[" to last "}" or "]" (drops surrounding prose; lets
67
+ // jsonrepair fix escaping inside) — root ARRAYS matter for array schemas
68
+ // 3. first "{" or "[" to end of text (TRUNCATED output —
69
+ // finishReason=length — where the closing bracket was cut off;
70
+ // jsonrepair closes it)
71
+ // `truncated` marks the first-open-to-end candidate: it is only reachable
72
+ // when no balanced span and no first-to-last span matched, i.e. there was no
73
+ // closing bracket at all — the signature of token-truncated output.
74
+ const candidates = [];
75
+ let searchFrom = 0;
76
+ for (;;) {
77
+ const found = nextBalancedJsonSpan(text, searchFrom);
78
+ if (!found) {
79
+ break;
80
+ }
81
+ candidates.push({ text: found.span, truncated: false });
82
+ searchFrom = found.end;
83
+ }
84
+ const openIndexes = [text.indexOf("{"), text.indexOf("[")].filter((i) => i >= 0);
85
+ const firstOpen = openIndexes.length > 0 ? Math.min(...openIndexes) : -1;
86
+ const lastClose = Math.max(text.lastIndexOf("}"), text.lastIndexOf("]"));
87
+ if (firstOpen >= 0 && lastClose > firstOpen) {
88
+ candidates.push({
89
+ text: text.slice(firstOpen, lastClose + 1),
90
+ truncated: false,
91
+ });
92
+ }
93
+ if (firstOpen >= 0) {
94
+ candidates.push({ text: text.slice(firstOpen), truncated: true });
95
+ }
96
+ let firstValid;
97
+ let schemaMatch;
98
+ const seen = new Set();
99
+ for (const candidate of candidates) {
100
+ if (seen.has(candidate.text)) {
101
+ continue;
102
+ }
103
+ seen.add(candidate.text);
104
+ const outcome = parseOrRepair(candidate.text);
105
+ if (outcome === undefined ||
106
+ outcome.value === null ||
107
+ typeof outcome.value !== "object") {
108
+ continue;
109
+ }
110
+ const record = {
111
+ value: outcome.value,
112
+ repaired: outcome.repaired,
113
+ truncated: candidate.truncated,
114
+ };
115
+ if (firstValid === undefined) {
116
+ firstValid = record;
117
+ }
118
+ if (schema && hasSafeParse(schema)) {
119
+ const safeParseable = schema;
120
+ if (safeParseable.safeParse(outcome.value).success) {
121
+ schemaMatch = record;
122
+ break;
123
+ }
124
+ }
125
+ else {
126
+ // No Zod schema to discriminate — first parseable object wins.
127
+ break;
128
+ }
129
+ }
130
+ const chosen = schemaMatch ?? firstValid;
131
+ if (chosen === undefined) {
132
+ return null;
133
+ }
134
+ return {
135
+ content: JSON.stringify(chosen.value),
136
+ structuredData: chosen.value,
137
+ repaired: chosen.repaired,
138
+ truncated: chosen.truncated,
139
+ };
140
+ }
@@ -4,6 +4,16 @@
4
4
  * Utilities for extracting JSON from mixed text content.
5
5
  * Particularly useful for parsing AI responses that contain JSON within prose.
6
6
  */
7
+ /**
8
+ * Find the first balanced JSON object/array span starting at or after
9
+ * `fromIndex`. Quote- and escape-aware: braces inside string literals do not
10
+ * affect depth. Returns the matched substring and the index just past it, or
11
+ * null if no balanced span exists.
12
+ */
13
+ export declare function nextBalancedJsonSpan(text: string, fromIndex?: number): {
14
+ span: string;
15
+ end: number;
16
+ } | null;
7
17
  /**
8
18
  * Extract JSON string from text that may contain surrounding content.
9
19
  *
@@ -5,6 +5,53 @@
5
5
  * Particularly useful for parsing AI responses that contain JSON within prose.
6
6
  */
7
7
  import { parseJsonOrNull } from "./safeParse.js";
8
+ /**
9
+ * Find the first balanced JSON object/array span starting at or after
10
+ * `fromIndex`. Quote- and escape-aware: braces inside string literals do not
11
+ * affect depth. Returns the matched substring and the index just past it, or
12
+ * null if no balanced span exists.
13
+ */
14
+ export function nextBalancedJsonSpan(text, fromIndex = 0) {
15
+ for (let start = fromIndex; start < text.length; start++) {
16
+ const openChar = text[start];
17
+ if (openChar !== "{" && openChar !== "[") {
18
+ continue;
19
+ }
20
+ const closeChar = openChar === "{" ? "}" : "]";
21
+ let depth = 0;
22
+ let inString = false;
23
+ let escapeNext = false;
24
+ for (let i = start; i < text.length; i++) {
25
+ const ch = text[i];
26
+ if (escapeNext) {
27
+ escapeNext = false;
28
+ continue;
29
+ }
30
+ if (ch === "\\") {
31
+ escapeNext = true;
32
+ continue;
33
+ }
34
+ if (ch === '"') {
35
+ inString = !inString;
36
+ continue;
37
+ }
38
+ if (inString) {
39
+ continue;
40
+ }
41
+ if (ch === openChar) {
42
+ depth++;
43
+ }
44
+ else if (ch === closeChar) {
45
+ depth--;
46
+ if (depth === 0) {
47
+ return { span: text.substring(start, i + 1), end: i + 1 };
48
+ }
49
+ }
50
+ }
51
+ // Unbalanced from this start — try the next opening char.
52
+ }
53
+ return null;
54
+ }
8
55
  /**
9
56
  * Extract JSON string from text that may contain surrounding content.
10
57
  *
@@ -45,21 +92,24 @@ export function extractJsonStringFromText(text) {
45
92
  // Continue to other patterns
46
93
  }
47
94
  }
48
- // Try to find JSON object or array pattern using non-greedy iterative scan.
49
- // Note: [\s\S]*? is non-greedy but can still produce over-spanning matches
50
- // in texts with many braces. This is acceptable as we try-parse each candidate
51
- // and move to the next on failure. A bracket-balancing parser would be more
52
- // precise but significantly more complex for marginal benefit.
53
- const candidateRegex = /(\{[\s\S]*?\}|\[[\s\S]*?\])/g;
54
- let candidate;
55
- while ((candidate = candidateRegex.exec(text)) !== null) {
95
+ // Scan for balanced JSON object/array spans (quote/escape aware) and return
96
+ // the first one that parses. Unlike a non-greedy regex, this never stops at a
97
+ // "}" that lives inside a string value, so nested objects are preserved.
98
+ let searchFrom = 0;
99
+ for (;;) {
100
+ const found = nextBalancedJsonSpan(text, searchFrom);
101
+ if (!found) {
102
+ break;
103
+ }
56
104
  try {
57
- JSON.parse(candidate[1]);
58
- return candidate[1];
105
+ JSON.parse(found.span);
106
+ return found.span;
59
107
  }
60
108
  catch {
61
- // Try next candidate
109
+ // Not valid JSON — resume scanning just past this opening character so a
110
+ // valid inner object/array can still be found.
62
111
  }
112
+ searchFrom = found.end - found.span.length + 1;
63
113
  }
64
114
  return null;
65
115
  }
@@ -18,3 +18,20 @@ export declare function hasRestrictedOutputLimit(modelName: string): boolean;
18
18
  * Get the max output tokens for a model (32768 for restricted models)
19
19
  */
20
20
  export declare const RESTRICTED_OUTPUT_TOKEN_LIMIT = 32768;
21
+ /**
22
+ * Normalize an Anthropic-API-style Claude model ID to the Vertex publisher
23
+ * format.
24
+ *
25
+ * The Anthropic API dates models with a trailing dash segment
26
+ * ("claude-haiku-4-5-20251001") while Vertex publisher IDs separate the date
27
+ * with "@" ("claude-haiku-4-5@20251001"). Vertex rejects the dash form with a
28
+ * 404 (verified live against us-east5), so the native Vertex+Claude paths
29
+ * normalize before calling @anthropic-ai/vertex-sdk.
30
+ *
31
+ * Pass-through cases: IDs already in "@" form, bare aliases with no date
32
+ * ("claude-sonnet-4-6" — Vertex resolves these itself), and non-Claude models.
33
+ * Legacy v2-suffixed Vertex IDs ("claude-3-5-sonnet-v2@20241022") have no
34
+ * dash-date equivalent, so those legacy dash IDs stay out of scope: they 404
35
+ * today and still 404 after the transform — no regression either way.
36
+ */
37
+ export declare function toVertexAnthropicModelId(modelName: string): string;
@@ -105,3 +105,26 @@ export function hasRestrictedOutputLimit(modelName) {
105
105
  * Get the max output tokens for a model (32768 for restricted models)
106
106
  */
107
107
  export const RESTRICTED_OUTPUT_TOKEN_LIMIT = 32768;
108
+ /**
109
+ * Normalize an Anthropic-API-style Claude model ID to the Vertex publisher
110
+ * format.
111
+ *
112
+ * The Anthropic API dates models with a trailing dash segment
113
+ * ("claude-haiku-4-5-20251001") while Vertex publisher IDs separate the date
114
+ * with "@" ("claude-haiku-4-5@20251001"). Vertex rejects the dash form with a
115
+ * 404 (verified live against us-east5), so the native Vertex+Claude paths
116
+ * normalize before calling @anthropic-ai/vertex-sdk.
117
+ *
118
+ * Pass-through cases: IDs already in "@" form, bare aliases with no date
119
+ * ("claude-sonnet-4-6" — Vertex resolves these itself), and non-Claude models.
120
+ * Legacy v2-suffixed Vertex IDs ("claude-3-5-sonnet-v2@20241022") have no
121
+ * dash-date equivalent, so those legacy dash IDs stay out of scope: they 404
122
+ * today and still 404 after the transform — no regression either way.
123
+ */
124
+ export function toVertexAnthropicModelId(modelName) {
125
+ if (!modelName.startsWith("claude-") || modelName.includes("@")) {
126
+ return modelName;
127
+ }
128
+ const dashDate = modelName.match(/^(claude-[a-z0-9-]+)-(\d{8})$/);
129
+ return dashDate ? `${dashDate[1]}@${dashDate[2]}` : modelName;
130
+ }
@@ -7,6 +7,26 @@ import { PROVIDER_MAX_TOKENS } from "../core/constants.js";
7
7
  * Get the safe maximum tokens for a provider and model
8
8
  */
9
9
  export declare function getSafeMaxTokens(provider: keyof typeof PROVIDER_MAX_TOKENS | string, model?: string, requestedMaxTokens?: number): number | undefined;
10
+ /**
11
+ * Maximum output tokens supported by a given Anthropic Claude model.
12
+ *
13
+ * The native Vertex+Claude and native Anthropic message paths send `max_tokens`
14
+ * straight to the Anthropic API, which returns 400 if the value exceeds the
15
+ * model's published output ceiling. (The AI-SDK path clamps automatically;
16
+ * these native paths do not.) This table lets those paths default to the
17
+ * model's real ceiling — 64K for Sonnet/Haiku 4.x, 32K for Opus 4.x — instead of
18
+ * the legacy 4096 that silently truncated large structured responses.
19
+ *
20
+ * Unknown identifiers fall back to a safe modern floor (8192).
21
+ */
22
+ export declare function getClaudeMaxOutputTokens(model: string | undefined): number;
23
+ /**
24
+ * Resolve the `max_tokens` to send on a native Anthropic/Claude request: honour
25
+ * the caller's value but clamp it to the model's published ceiling, and default
26
+ * to that ceiling when the caller did not specify one. Prevents both silent
27
+ * truncation (the legacy 4096 default) and 400s from over-large requests.
28
+ */
29
+ export declare function resolveClaudeMaxTokens(model: string | undefined, requested?: number): number;
10
30
  /**
11
31
  * Validate if maxTokens is safe for a provider/model combination
12
32
  */
@@ -76,6 +76,61 @@ export function getSafeMaxTokens(provider, model, requestedMaxTokens) {
76
76
  // Use the requested value if it's within limits
77
77
  return requestedMaxTokens;
78
78
  }
79
+ /**
80
+ * Maximum output tokens supported by a given Anthropic Claude model.
81
+ *
82
+ * The native Vertex+Claude and native Anthropic message paths send `max_tokens`
83
+ * straight to the Anthropic API, which returns 400 if the value exceeds the
84
+ * model's published output ceiling. (The AI-SDK path clamps automatically;
85
+ * these native paths do not.) This table lets those paths default to the
86
+ * model's real ceiling — 64K for Sonnet/Haiku 4.x, 32K for Opus 4.x — instead of
87
+ * the legacy 4096 that silently truncated large structured responses.
88
+ *
89
+ * Unknown identifiers fall back to a safe modern floor (8192).
90
+ */
91
+ export function getClaudeMaxOutputTokens(model) {
92
+ const m = (model ?? "").toLowerCase();
93
+ // Claude 4.x family: Opus 4.x = 32K, Sonnet/Haiku 4.x = 64K.
94
+ if (/opus[-_.]?4/.test(m)) {
95
+ return 32000;
96
+ }
97
+ if (/sonnet[-_.]?4/.test(m) || /haiku[-_.]?4/.test(m)) {
98
+ return 64000;
99
+ }
100
+ // Claude 3.7 Sonnet supports 64K output.
101
+ if (/3[-_.]?7[-_.]?sonnet/.test(m)) {
102
+ return 64000;
103
+ }
104
+ // Claude 3.5 Sonnet / Haiku → 8192.
105
+ if (/3[-_.]?5[-_.]?(sonnet|haiku)/.test(m)) {
106
+ return 8192;
107
+ }
108
+ // Claude 3 Opus / Sonnet / Haiku → 4096.
109
+ if (/claude-3-(opus|sonnet|haiku)/.test(m) || /3[-_.]?opus/.test(m)) {
110
+ return 4096;
111
+ }
112
+ // Bare family aliases (latest of a family) → assume the modern ceiling.
113
+ if (m.includes("opus")) {
114
+ return 32000;
115
+ }
116
+ if (m.includes("sonnet") || m.includes("haiku")) {
117
+ return 64000;
118
+ }
119
+ return 8192;
120
+ }
121
+ /**
122
+ * Resolve the `max_tokens` to send on a native Anthropic/Claude request: honour
123
+ * the caller's value but clamp it to the model's published ceiling, and default
124
+ * to that ceiling when the caller did not specify one. Prevents both silent
125
+ * truncation (the legacy 4096 default) and 400s from over-large requests.
126
+ */
127
+ export function resolveClaudeMaxTokens(model, requested) {
128
+ const ceiling = getClaudeMaxOutputTokens(model);
129
+ if (requested !== undefined && requested !== null && requested > 0) {
130
+ return Math.min(requested, ceiling);
131
+ }
132
+ return ceiling;
133
+ }
79
134
  /**
80
135
  * Validate if maxTokens is safe for a provider/model combination
81
136
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.70.0",
3
+ "version": "9.70.2",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applications with 21+ providers: OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, Azure OpenAI, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible, OpenRouter, DeepSeek, NVIDIA NIM, LM Studio, llama.cpp, plus voice (OpenAI TTS, ElevenLabs, Deepgram, Azure Speech).",
6
6
  "author": {
@@ -108,6 +108,8 @@
108
108
  "test:dynamic": "npx tsx test/continuous-test-suite-dynamic.ts",
109
109
  "test:proxy": "npx tsx test/continuous-test-suite-proxy.ts",
110
110
  "test:bugfixes": "npx tsx test/continuous-test-suite-bugfixes.ts",
111
+ "test:json": "npx tsx test/continuous-test-suite-json.ts",
112
+ "test:json-e2e": "npx tsx test/continuous-test-suite-json-e2e.ts",
111
113
  "test:workflow": "npx tsx test/continuous-test-suite-workflow.ts",
112
114
  "test:hitl": "npx tsx test/continuous-test-suite-hitl.ts",
113
115
  "test:tasks": "npx tsx test/continuous-test-suite-tasks.ts",
@@ -341,6 +343,7 @@
341
343
  "inquirer": "^13.3.0",
342
344
  "jose": "^6.1.3",
343
345
  "json-schema-to-zod": "^2.7.0",
346
+ "jsonrepair": "^3.14.0",
344
347
  "nanoid": "^5.1.5",
345
348
  "open": "^11.0.0",
346
349
  "ora": "^9.3.0",