@juspay/neurolink 9.54.1 → 9.54.3
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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +288 -288
- package/dist/cli/factories/commandFactory.js +43 -4
- package/dist/cli/utils/abortHandler.d.ts +22 -0
- package/dist/cli/utils/abortHandler.js +53 -0
- package/dist/core/baseProvider.d.ts +7 -1
- package/dist/core/baseProvider.js +19 -0
- package/dist/lib/core/baseProvider.d.ts +7 -1
- package/dist/lib/core/baseProvider.js +19 -0
- package/dist/lib/neurolink.js +17 -1
- package/dist/lib/providers/anthropic.js +1 -0
- package/dist/lib/providers/anthropicBaseProvider.js +1 -0
- package/dist/lib/providers/azureOpenai.js +1 -0
- package/dist/lib/providers/googleAiStudio.js +1 -0
- package/dist/lib/providers/googleVertex.d.ts +14 -0
- package/dist/lib/providers/googleVertex.js +51 -12
- package/dist/lib/providers/huggingFace.js +1 -0
- package/dist/lib/providers/litellm.js +1 -0
- package/dist/lib/providers/mistral.js +1 -0
- package/dist/lib/providers/openAI.js +1 -0
- package/dist/lib/providers/openRouter.js +1 -0
- package/dist/lib/providers/openaiCompatible.js +1 -0
- package/dist/lib/proxy/routingPolicy.d.ts +27 -17
- package/dist/lib/proxy/routingPolicy.js +53 -209
- package/dist/lib/server/routes/claudeProxyRoutes.js +35 -73
- package/dist/lib/types/proxyTypes.d.ts +9 -50
- package/dist/lib/types/streamTypes.d.ts +6 -0
- package/dist/lib/utils/messageBuilder.js +39 -6
- package/dist/lib/utils/toolCallRepair.d.ts +21 -0
- package/dist/lib/utils/toolCallRepair.js +298 -0
- package/dist/neurolink.js +17 -1
- package/dist/providers/anthropic.js +1 -0
- package/dist/providers/anthropicBaseProvider.js +1 -0
- package/dist/providers/azureOpenai.js +1 -0
- package/dist/providers/googleAiStudio.js +1 -0
- package/dist/providers/googleVertex.d.ts +14 -0
- package/dist/providers/googleVertex.js +51 -12
- package/dist/providers/huggingFace.js +1 -0
- package/dist/providers/litellm.js +1 -0
- package/dist/providers/mistral.js +1 -0
- package/dist/providers/openAI.js +1 -0
- package/dist/providers/openRouter.js +1 -0
- package/dist/providers/openaiCompatible.js +1 -0
- package/dist/proxy/routingPolicy.d.ts +27 -17
- package/dist/proxy/routingPolicy.js +53 -209
- package/dist/server/routes/claudeProxyRoutes.js +35 -73
- package/dist/types/proxyTypes.d.ts +9 -50
- package/dist/types/streamTypes.d.ts +6 -0
- package/dist/utils/messageBuilder.js +39 -6
- package/dist/utils/toolCallRepair.d.ts +21 -0
- package/dist/utils/toolCallRepair.js +297 -0
- package/package.json +1 -1
|
@@ -325,6 +325,8 @@ export type StreamOptions = {
|
|
|
325
325
|
/** AbortSignal for external cancellation of the AI call */
|
|
326
326
|
abortSignal?: AbortSignal;
|
|
327
327
|
disableTools?: boolean;
|
|
328
|
+
/** Disable the schema-driven tool call repair mechanism (BZ-665). Default: false (repair enabled). */
|
|
329
|
+
disableToolCallRepair?: boolean;
|
|
328
330
|
maxSteps?: number;
|
|
329
331
|
/**
|
|
330
332
|
* Tool choice configuration for streaming generation.
|
|
@@ -432,6 +434,10 @@ export type StreamOptions = {
|
|
|
432
434
|
* @internal Set by NeuroLink SDK — not typically used directly by consumers.
|
|
433
435
|
*/
|
|
434
436
|
fileRegistry?: unknown;
|
|
437
|
+
/** BZ-1341: Override fallback provider name (takes precedence over env/model config). */
|
|
438
|
+
fallbackProvider?: string;
|
|
439
|
+
/** BZ-1341: Override fallback model name (takes precedence over env/model config). */
|
|
440
|
+
fallbackModel?: string;
|
|
435
441
|
/** Callback invoked when streaming completes successfully. */
|
|
436
442
|
onFinish?: OnFinishCallback;
|
|
437
443
|
/** Callback invoked when streaming encounters an error. */
|
|
@@ -350,11 +350,9 @@ export function convertToModelMessages(messages) {
|
|
|
350
350
|
// Assistant messages only support text content, filter out images
|
|
351
351
|
const textOnlyContent = validContent.filter((item) => item.type === "text");
|
|
352
352
|
if (textOnlyContent.length === 0) {
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
content: "",
|
|
357
|
-
};
|
|
353
|
+
// No text content (e.g., only images/files) — skip message
|
|
354
|
+
// to avoid sending empty content to providers like Claude
|
|
355
|
+
return null;
|
|
358
356
|
}
|
|
359
357
|
else if (textOnlyContent.length === 1) {
|
|
360
358
|
// Single text item, use string content
|
|
@@ -1073,9 +1071,44 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
|
|
|
1073
1071
|
msg.role === "assistant" ||
|
|
1074
1072
|
msg.role === "system") {
|
|
1075
1073
|
const providerOptions = msg.providerOptions;
|
|
1074
|
+
// Sanitize assistant array content: strip tool_use/tool_result blocks
|
|
1075
|
+
// that providers cannot handle. If an assistant message ends up empty
|
|
1076
|
+
// after stripping, skip it to avoid sending content: "" to Claude.
|
|
1077
|
+
// Only assistant messages need this — user messages may contain valid
|
|
1078
|
+
// image/file blocks that must pass through unchanged.
|
|
1079
|
+
let sanitizedContent = msg.content;
|
|
1080
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
1081
|
+
const textParts = msg.content.filter((item) => !!item &&
|
|
1082
|
+
typeof item === "object" &&
|
|
1083
|
+
item.type === "text" &&
|
|
1084
|
+
typeof item.text === "string");
|
|
1085
|
+
if (textParts.length === 0) {
|
|
1086
|
+
// All content was tool_use/tool_result/non-text — skip message
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
// Check if any retained text part carries providerOptions
|
|
1090
|
+
// (e.g. Anthropic cache_control). If so, preserve them as
|
|
1091
|
+
// array content to avoid losing per-block metadata.
|
|
1092
|
+
const hasItemProviderOptions = textParts.some((item) => !!item.providerOptions);
|
|
1093
|
+
if (hasItemProviderOptions) {
|
|
1094
|
+
sanitizedContent = textParts;
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
sanitizedContent =
|
|
1098
|
+
textParts.length === 1
|
|
1099
|
+
? textParts[0].text
|
|
1100
|
+
: textParts
|
|
1101
|
+
.map((p) => p.text)
|
|
1102
|
+
.join(" ");
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
// Skip empty string content to avoid Claude API rejection
|
|
1106
|
+
if (sanitizedContent === "") {
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1076
1109
|
messages.push({
|
|
1077
1110
|
role: msg.role,
|
|
1078
|
-
content:
|
|
1111
|
+
content: sanitizedContent,
|
|
1079
1112
|
...(providerOptions && { providerOptions }),
|
|
1080
1113
|
});
|
|
1081
1114
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-Driven Tool Call Repair (BZ-665)
|
|
3
|
+
*
|
|
4
|
+
* Implements `experimental_repairToolCall` for the Vercel AI SDK.
|
|
5
|
+
* When an LLM sends a wrong tool name or wrong parameter names,
|
|
6
|
+
* this module attempts deterministic, schema-driven repair:
|
|
7
|
+
*
|
|
8
|
+
* 1. Tool name: case-insensitive → substring → Levenshtein
|
|
9
|
+
* 2. Param names: compare against JSON schema properties dynamically
|
|
10
|
+
* 3. Type coercion: string→number, JSON string→object/array per schema
|
|
11
|
+
*
|
|
12
|
+
* Zero static alias maps. The tool's JSON schema is the only source of truth.
|
|
13
|
+
*
|
|
14
|
+
* @module utils/toolCallRepair
|
|
15
|
+
*/
|
|
16
|
+
import type { ToolCallRepairFunction, ToolSet } from "ai";
|
|
17
|
+
/**
|
|
18
|
+
* Create an `experimental_repairToolCall` handler for streamText/generateText.
|
|
19
|
+
* Fully dynamic — reads the tool schema at repair time, no configuration needed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createToolCallRepair(): ToolCallRepairFunction<ToolSet>;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-Driven Tool Call Repair (BZ-665)
|
|
3
|
+
*
|
|
4
|
+
* Implements `experimental_repairToolCall` for the Vercel AI SDK.
|
|
5
|
+
* When an LLM sends a wrong tool name or wrong parameter names,
|
|
6
|
+
* this module attempts deterministic, schema-driven repair:
|
|
7
|
+
*
|
|
8
|
+
* 1. Tool name: case-insensitive → substring → Levenshtein
|
|
9
|
+
* 2. Param names: compare against JSON schema properties dynamically
|
|
10
|
+
* 3. Type coercion: string→number, JSON string→object/array per schema
|
|
11
|
+
*
|
|
12
|
+
* Zero static alias maps. The tool's JSON schema is the only source of truth.
|
|
13
|
+
*
|
|
14
|
+
* @module utils/toolCallRepair
|
|
15
|
+
*/
|
|
16
|
+
import { logger } from "./logger.js";
|
|
17
|
+
/**
|
|
18
|
+
* Create an `experimental_repairToolCall` handler for streamText/generateText.
|
|
19
|
+
* Fully dynamic — reads the tool schema at repair time, no configuration needed.
|
|
20
|
+
*/
|
|
21
|
+
export function createToolCallRepair() {
|
|
22
|
+
return async ({ toolCall, tools, inputSchema, error }) => {
|
|
23
|
+
// Import error classes lazily to avoid circular deps at module level
|
|
24
|
+
const { NoSuchToolError: NoSuchTool, InvalidToolInputError: InvalidInput } = await import("ai");
|
|
25
|
+
if (NoSuchTool.isInstance(error)) {
|
|
26
|
+
return repairToolName(toolCall, Object.keys(tools));
|
|
27
|
+
}
|
|
28
|
+
if (InvalidInput.isInstance(error)) {
|
|
29
|
+
try {
|
|
30
|
+
const schema = await inputSchema({ toolName: toolCall.toolName });
|
|
31
|
+
return repairToolInput(toolCall, schema);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// inputSchema() failed — can't repair without schema
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// ─── Tool Name Repair ──────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Attempt to match a wrong tool name against available tool names.
|
|
44
|
+
* Strategies (in order): case-insensitive exact → substring → Levenshtein.
|
|
45
|
+
*/
|
|
46
|
+
function repairToolName(toolCall, availableTools) {
|
|
47
|
+
const called = toolCall.toolName;
|
|
48
|
+
// Guard: empty or whitespace-only tool name cannot be meaningfully repaired
|
|
49
|
+
if (!called || called.trim().length === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// 1. Case-insensitive exact match
|
|
53
|
+
const ciMatch = availableTools.find((t) => t.toLowerCase() === called.toLowerCase());
|
|
54
|
+
if (ciMatch) {
|
|
55
|
+
logger.debug(`[ToolCallRepair] Name repair (case): "${called}" → "${ciMatch}"`);
|
|
56
|
+
return { ...toolCall, toolName: ciMatch };
|
|
57
|
+
}
|
|
58
|
+
// 2. Substring match: "search_file" is substring of "search_files" or vice versa.
|
|
59
|
+
// Only accept when exactly one tool matches to avoid ambiguous repairs.
|
|
60
|
+
const calledLower = called.toLowerCase();
|
|
61
|
+
const subCandidates = availableTools.filter((t) => {
|
|
62
|
+
const tLower = t.toLowerCase();
|
|
63
|
+
return tLower.includes(calledLower) || calledLower.includes(tLower);
|
|
64
|
+
});
|
|
65
|
+
if (subCandidates.length === 1) {
|
|
66
|
+
logger.debug(`[ToolCallRepair] Name repair (substring): "${called}" → "${subCandidates[0]}"`);
|
|
67
|
+
return { ...toolCall, toolName: subCandidates[0] };
|
|
68
|
+
}
|
|
69
|
+
// 3. Levenshtein distance — accept if normalized distance < 0.3
|
|
70
|
+
// Compare by normalized score (not raw edits) so length differences don't skew selection.
|
|
71
|
+
let bestMatch = null;
|
|
72
|
+
let bestNormalized = Infinity;
|
|
73
|
+
for (const t of availableTools) {
|
|
74
|
+
const dist = levenshtein(calledLower, t.toLowerCase());
|
|
75
|
+
const maxLen = Math.max(called.length, t.length);
|
|
76
|
+
const normalized = maxLen === 0 ? 0 : dist / maxLen;
|
|
77
|
+
if (normalized < 0.3 && normalized < bestNormalized) {
|
|
78
|
+
bestNormalized = normalized;
|
|
79
|
+
bestMatch = t;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (bestMatch) {
|
|
83
|
+
logger.debug(`[ToolCallRepair] Name repair (levenshtein ${bestNormalized.toFixed(2)}): "${called}" → "${bestMatch}"`);
|
|
84
|
+
return { ...toolCall, toolName: bestMatch };
|
|
85
|
+
}
|
|
86
|
+
logger.debug(`[ToolCallRepair] Could not repair tool name "${called}". Available: [${availableTools.join(", ")}]`);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
// ─── Tool Input Repair ─────────────────────────────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Attempt to repair wrong parameter names and types using the JSON schema.
|
|
92
|
+
* Compares LLM-provided keys against schema properties dynamically.
|
|
93
|
+
*
|
|
94
|
+
* `toolCall.input` is a JSON string per LanguageModelV3ToolCall.
|
|
95
|
+
*/
|
|
96
|
+
function repairToolInput(toolCall, schema) {
|
|
97
|
+
let args;
|
|
98
|
+
try {
|
|
99
|
+
args = JSON.parse(toolCall.input);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null; // input is not valid JSON — can't repair
|
|
103
|
+
}
|
|
104
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const schemaProps = schema.properties;
|
|
108
|
+
if (!schemaProps) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const expectedKeys = Object.keys(schemaProps);
|
|
112
|
+
const inputObj = args;
|
|
113
|
+
const inputKeys = Object.keys(inputObj);
|
|
114
|
+
const repaired = Object.create(null);
|
|
115
|
+
let didRepair = false;
|
|
116
|
+
const dropUnknown = schema.additionalProperties === false;
|
|
117
|
+
for (const inputKey of inputKeys) {
|
|
118
|
+
// Already matches a schema property — keep as-is
|
|
119
|
+
if (expectedKeys.includes(inputKey)) {
|
|
120
|
+
repaired[inputKey] = inputObj[inputKey];
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Try to find a matching schema property
|
|
124
|
+
const mapped = findMatchingKey(inputKey, expectedKeys);
|
|
125
|
+
if (mapped) {
|
|
126
|
+
// Don't overwrite an already-populated canonical key — but still mark as repaired
|
|
127
|
+
// so the function returns the corrected object instead of null.
|
|
128
|
+
if (Object.prototype.hasOwnProperty.call(repaired, mapped)) {
|
|
129
|
+
didRepair = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
logger.debug(`[ToolCallRepair] Param repair: "${inputKey}" → "${mapped}" (tool: ${toolCall.toolName})`);
|
|
133
|
+
repaired[mapped] = inputObj[inputKey];
|
|
134
|
+
didRepair = true;
|
|
135
|
+
}
|
|
136
|
+
else if (dropUnknown) {
|
|
137
|
+
// Schema forbids extra properties — drop unmapped keys
|
|
138
|
+
logger.debug(`[ToolCallRepair] Dropping unmapped key "${inputKey}" (additionalProperties: false, tool: ${toolCall.toolName})`);
|
|
139
|
+
didRepair = true;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// Unknown key — pass through (schema allows additionalProperties)
|
|
143
|
+
repaired[inputKey] = inputObj[inputKey];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Type coercion based on schema types
|
|
147
|
+
for (const key of Object.keys(repaired)) {
|
|
148
|
+
const propSchema = schemaProps[key];
|
|
149
|
+
if (!propSchema) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const coerced = coerceType(repaired[key], propSchema);
|
|
153
|
+
if (coerced !== repaired[key]) {
|
|
154
|
+
logger.debug(`[ToolCallRepair] Type coercion on "${key}": ${typeof repaired[key]} → ${typeof coerced} (tool: ${toolCall.toolName})`);
|
|
155
|
+
repaired[key] = coerced;
|
|
156
|
+
didRepair = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (didRepair) {
|
|
160
|
+
return { ...toolCall, input: JSON.stringify(repaired) };
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Find a matching schema key for a mismatched input key.
|
|
166
|
+
* Strategies: case-insensitive → Levenshtein (threshold ≤2 edits).
|
|
167
|
+
*/
|
|
168
|
+
function findMatchingKey(inputKey, schemaKeys) {
|
|
169
|
+
const inputLower = inputKey.toLowerCase();
|
|
170
|
+
// Case-insensitive match
|
|
171
|
+
const ciMatch = schemaKeys.find((k) => k.toLowerCase() === inputLower);
|
|
172
|
+
if (ciMatch) {
|
|
173
|
+
return ciMatch;
|
|
174
|
+
}
|
|
175
|
+
// Levenshtein — threshold ≤2 edits
|
|
176
|
+
let best = null;
|
|
177
|
+
let bestDist = Infinity;
|
|
178
|
+
for (const k of schemaKeys) {
|
|
179
|
+
const dist = levenshtein(inputLower, k.toLowerCase());
|
|
180
|
+
if (dist <= 2 && dist < bestDist) {
|
|
181
|
+
bestDist = dist;
|
|
182
|
+
best = k;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return best;
|
|
186
|
+
}
|
|
187
|
+
// ─── Type Coercion ─────────────────────────────────────────────────
|
|
188
|
+
/**
|
|
189
|
+
* Coerce a value to match the expected schema type.
|
|
190
|
+
* Handles: string→number, JSON string→object, JSON string→array, value→[value].
|
|
191
|
+
*/
|
|
192
|
+
function coerceType(value, propSchema) {
|
|
193
|
+
const expectedType = propSchema.type;
|
|
194
|
+
if (!expectedType || value === null || value === undefined) {
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
// String → Number (trim first, reject empty/whitespace, require finite result)
|
|
198
|
+
if (expectedType === "number" && typeof value === "string") {
|
|
199
|
+
const trimmed = value.trim();
|
|
200
|
+
if (trimmed !== "") {
|
|
201
|
+
const num = Number(trimmed);
|
|
202
|
+
if (isFinite(num)) {
|
|
203
|
+
return num;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// String → Integer (strict: reject "12abc", "3.7", etc.)
|
|
208
|
+
if (expectedType === "integer" && typeof value === "string") {
|
|
209
|
+
const trimmed = value.trim();
|
|
210
|
+
if (/^[+-]?\d+$/.test(trimmed)) {
|
|
211
|
+
const num = Number(trimmed);
|
|
212
|
+
if (Number.isSafeInteger(num)) {
|
|
213
|
+
return num;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// String → Boolean
|
|
218
|
+
if (expectedType === "boolean" && typeof value === "string") {
|
|
219
|
+
if (value.toLowerCase() === "true") {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (value.toLowerCase() === "false") {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// JSON string → Object
|
|
227
|
+
if (expectedType === "object" && typeof value === "string") {
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(value);
|
|
230
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
231
|
+
return parsed;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Not valid JSON — return as-is
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// JSON string → Array
|
|
239
|
+
if (expectedType === "array" && typeof value === "string") {
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(value);
|
|
242
|
+
if (Array.isArray(parsed)) {
|
|
243
|
+
return parsed;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Not valid JSON — return as-is
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Single non-string value → Array (wrap).
|
|
251
|
+
// Strings are excluded because they are more likely a JSON-encoded array
|
|
252
|
+
// that failed to parse above, and wrapping "foo" into ["foo"] is rarely correct.
|
|
253
|
+
if (expectedType === "array" &&
|
|
254
|
+
!Array.isArray(value) &&
|
|
255
|
+
typeof value !== "string") {
|
|
256
|
+
return [value];
|
|
257
|
+
}
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
// ─── Levenshtein Distance ──────────────────────────────────────────
|
|
261
|
+
/**
|
|
262
|
+
* Compute Levenshtein edit distance between two strings.
|
|
263
|
+
* Uses the iterative matrix approach — O(m*n) time, O(min(m,n)) space.
|
|
264
|
+
*/
|
|
265
|
+
function levenshtein(a, b) {
|
|
266
|
+
if (a === b) {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
if (a.length === 0) {
|
|
270
|
+
return b.length;
|
|
271
|
+
}
|
|
272
|
+
if (b.length === 0) {
|
|
273
|
+
return a.length;
|
|
274
|
+
}
|
|
275
|
+
// Use shorter string as column to minimize space
|
|
276
|
+
if (a.length > b.length) {
|
|
277
|
+
[a, b] = [b, a];
|
|
278
|
+
}
|
|
279
|
+
const aLen = a.length;
|
|
280
|
+
const bLen = b.length;
|
|
281
|
+
let prev = new Array(aLen + 1);
|
|
282
|
+
let curr = new Array(aLen + 1);
|
|
283
|
+
for (let i = 0; i <= aLen; i++) {
|
|
284
|
+
prev[i] = i;
|
|
285
|
+
}
|
|
286
|
+
for (let j = 1; j <= bLen; j++) {
|
|
287
|
+
curr[0] = j;
|
|
288
|
+
for (let i = 1; i <= aLen; i++) {
|
|
289
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
290
|
+
curr[i] = Math.min(prev[i] + 1, // deletion
|
|
291
|
+
curr[i - 1] + 1, // insertion
|
|
292
|
+
prev[i - 1] + cost);
|
|
293
|
+
}
|
|
294
|
+
[prev, curr] = [curr, prev];
|
|
295
|
+
}
|
|
296
|
+
return prev[aLen];
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=toolCallRepair.js.map
|
package/dist/neurolink.js
CHANGED
|
@@ -4831,15 +4831,31 @@ Current user's request: ${currentInput}`;
|
|
|
4831
4831
|
catch {
|
|
4832
4832
|
/* non-blocking */
|
|
4833
4833
|
}
|
|
4834
|
-
|
|
4834
|
+
// BZ-1341: Support fallback provider override via options or env vars
|
|
4835
|
+
const optFallbackProvider = enhancedOptions.fallbackProvider?.trim() || undefined;
|
|
4836
|
+
const optFallbackModel = enhancedOptions.fallbackModel?.trim() || undefined;
|
|
4837
|
+
const envFallbackProvider = process.env.FALLBACK_PROVIDER?.trim() || undefined;
|
|
4838
|
+
const envFallbackModel = process.env.FALLBACK_MODEL?.trim() || undefined;
|
|
4839
|
+
const modelConfigRoute = ModelRouter.getFallbackRoute(originalPrompt || enhancedOptions.input.text || "", {
|
|
4835
4840
|
provider: providerName,
|
|
4836
4841
|
model: enhancedOptions.model || "gpt-4o",
|
|
4837
4842
|
reasoning: "primary failed",
|
|
4838
4843
|
confidence: 0.5,
|
|
4839
4844
|
}, { fallbackStrategy: "auto" });
|
|
4845
|
+
const fallbackRoute = {
|
|
4846
|
+
...modelConfigRoute,
|
|
4847
|
+
provider: optFallbackProvider ?? envFallbackProvider ?? modelConfigRoute.provider,
|
|
4848
|
+
model: optFallbackModel ?? envFallbackModel ?? modelConfigRoute.model,
|
|
4849
|
+
};
|
|
4840
4850
|
logger.warn("Retrying with fallback provider", {
|
|
4841
4851
|
originalProvider: providerName,
|
|
4842
4852
|
fallbackProvider: fallbackRoute.provider,
|
|
4853
|
+
fallbackModel: fallbackRoute.model,
|
|
4854
|
+
fallbackSource: optFallbackProvider || optFallbackModel
|
|
4855
|
+
? "options"
|
|
4856
|
+
: envFallbackProvider || envFallbackModel
|
|
4857
|
+
? "env"
|
|
4858
|
+
: "model_config",
|
|
4843
4859
|
reason: errorMsg,
|
|
4844
4860
|
});
|
|
4845
4861
|
try {
|
|
@@ -799,6 +799,7 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
799
799
|
stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
|
|
800
800
|
toolChoice: resolveToolChoice(options, tools, shouldUseTools),
|
|
801
801
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
802
|
+
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
802
803
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
803
804
|
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
804
805
|
this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
|
|
@@ -94,6 +94,7 @@ export class AnthropicProviderV2 extends BaseProvider {
|
|
|
94
94
|
toolChoice: resolveToolChoice(options, tools, shouldUseTools),
|
|
95
95
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
96
96
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
97
|
+
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
97
98
|
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
98
99
|
this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
|
|
99
100
|
logger.warn("[AnthropicBaseProvider] Failed to store tool executions", {
|
|
@@ -124,6 +124,7 @@ export class AzureOpenAIProvider extends BaseProvider {
|
|
|
124
124
|
stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
|
|
125
125
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
126
126
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
127
|
+
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
127
128
|
onStepFinish: (event) => {
|
|
128
129
|
this.handleToolExecutionStorage([...event.toolCalls], [...event.toolResults], options, new Date()).catch((error) => {
|
|
129
130
|
logger.warn("[AzureOpenaiProvider] Failed to store tool executions", {
|
|
@@ -478,6 +478,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
478
478
|
toolChoice: resolveToolChoice(options, tools, shouldUseTools),
|
|
479
479
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
480
480
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
481
|
+
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
481
482
|
// Gemini 3: use thinkingLevel via providerOptions
|
|
482
483
|
// Gemini 2.5: use thinkingBudget via providerOptions
|
|
483
484
|
...(options.thinkingConfig?.enabled && {
|
|
@@ -5,6 +5,20 @@ import { BaseProvider } from "../core/baseProvider.js";
|
|
|
5
5
|
import type { EnhancedGenerateResult, TextGenerationOptions } from "../types/generateTypes.js";
|
|
6
6
|
import type { NeurolinkCredentials } from "../types/providers.js";
|
|
7
7
|
import type { StreamOptions, StreamResult } from "../types/streamTypes.js";
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the correct Vertex AI location for a given model.
|
|
10
|
+
*
|
|
11
|
+
* Google-published models (gemini-*) require the global endpoint
|
|
12
|
+
* (`aiplatform.googleapis.com`), not regional endpoints like
|
|
13
|
+
* `us-east5-aiplatform.googleapis.com`. Regional endpoints return
|
|
14
|
+
* "model not found" for these models.
|
|
15
|
+
*
|
|
16
|
+
* Anthropic-on-Vertex models (claude-*) require regional endpoints
|
|
17
|
+
* and are handled separately by `createVertexAnthropicSettings`.
|
|
18
|
+
*
|
|
19
|
+
* Embedding models and custom models use the configured location as-is.
|
|
20
|
+
*/
|
|
21
|
+
export declare const resolveVertexLocation: (modelName: string | undefined, configuredLocation: string) => string;
|
|
8
22
|
/**
|
|
9
23
|
* Vertex Model Aliases
|
|
10
24
|
*
|
|
@@ -79,7 +79,36 @@ const getVertexLocation = () => {
|
|
|
79
79
|
return (process.env.GOOGLE_CLOUD_LOCATION ||
|
|
80
80
|
process.env.VERTEX_LOCATION ||
|
|
81
81
|
process.env.GOOGLE_VERTEX_LOCATION ||
|
|
82
|
-
"
|
|
82
|
+
"global");
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the correct Vertex AI location for a given model.
|
|
86
|
+
*
|
|
87
|
+
* Google-published models (gemini-*) require the global endpoint
|
|
88
|
+
* (`aiplatform.googleapis.com`), not regional endpoints like
|
|
89
|
+
* `us-east5-aiplatform.googleapis.com`. Regional endpoints return
|
|
90
|
+
* "model not found" for these models.
|
|
91
|
+
*
|
|
92
|
+
* Anthropic-on-Vertex models (claude-*) require regional endpoints
|
|
93
|
+
* and are handled separately by `createVertexAnthropicSettings`.
|
|
94
|
+
*
|
|
95
|
+
* Embedding models and custom models use the configured location as-is.
|
|
96
|
+
*/
|
|
97
|
+
export const resolveVertexLocation = (modelName, configuredLocation) => {
|
|
98
|
+
if (!modelName) {
|
|
99
|
+
return configuredLocation;
|
|
100
|
+
}
|
|
101
|
+
const normalized = modelName.toLowerCase();
|
|
102
|
+
// Google-published models always use the global endpoint.
|
|
103
|
+
// Hardcoded because Google's Vertex AI serves Gemini models exclusively
|
|
104
|
+
// from the global endpoint — regional endpoints like us-east5 return
|
|
105
|
+
// "Publisher Model was not found" errors. The env var GOOGLE_VERTEX_LOCATION
|
|
106
|
+
// is typically set for Anthropic-on-Vertex (which needs regional), so we
|
|
107
|
+
// cannot rely on it for Gemini routing.
|
|
108
|
+
if (normalized.startsWith("gemini-")) {
|
|
109
|
+
return "global";
|
|
110
|
+
}
|
|
111
|
+
return configuredLocation;
|
|
83
112
|
};
|
|
84
113
|
const getDefaultVertexModel = () => {
|
|
85
114
|
// Use gemini-2.5-flash as default - latest and best price-performance model
|
|
@@ -96,8 +125,9 @@ const hasGoogleCredentials = () => {
|
|
|
96
125
|
// Module-level cache for runtime-created credentials file to avoid per-request writes
|
|
97
126
|
let cachedCredentialsPath = null;
|
|
98
127
|
// Enhanced Vertex settings creation with authentication fallback and proxy support
|
|
99
|
-
const createVertexSettings = async (region, credentials) => {
|
|
100
|
-
const
|
|
128
|
+
const createVertexSettings = async (region, credentials, modelName) => {
|
|
129
|
+
const configuredLocation = credentials?.location || region || getVertexLocation();
|
|
130
|
+
const location = resolveVertexLocation(modelName, configuredLocation);
|
|
101
131
|
const project = credentials?.projectId || getVertexProjectId();
|
|
102
132
|
const baseSettings = {
|
|
103
133
|
project,
|
|
@@ -326,7 +356,12 @@ const createVertexAnthropicSettings = async (region, credentials) => {
|
|
|
326
356
|
// which is invalid. The correct global endpoint omits the region prefix entirely.
|
|
327
357
|
// Since the SDK doesn't handle this, redirect "global" to "us-east5" for Anthropic.
|
|
328
358
|
const anthropicRegion = !region || region === "global" ? "us-east5" : region;
|
|
329
|
-
|
|
359
|
+
// Override credentials.location so it cannot conflict with the redirected
|
|
360
|
+
// region — createVertexSettings checks credentials.location first.
|
|
361
|
+
const anthropicCredentials = credentials?.location
|
|
362
|
+
? { ...credentials, location: anthropicRegion }
|
|
363
|
+
: credentials;
|
|
364
|
+
const baseVertexSettings = await createVertexSettings(anthropicRegion, anthropicCredentials);
|
|
330
365
|
// GoogleVertexAnthropicProviderSettings extends GoogleVertexProviderSettings
|
|
331
366
|
// so we can use the same settings with proper typing
|
|
332
367
|
return {
|
|
@@ -570,7 +605,9 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
570
605
|
networkConfig: {
|
|
571
606
|
projectId: this.projectId,
|
|
572
607
|
location: this.location,
|
|
573
|
-
expectedEndpoint:
|
|
608
|
+
expectedEndpoint: this.location === "global"
|
|
609
|
+
? "https://aiplatform.googleapis.com"
|
|
610
|
+
: `https://${this.location}-aiplatform.googleapis.com`,
|
|
574
611
|
httpProxy: process.env.HTTP_PROXY || process.env.http_proxy,
|
|
575
612
|
httpsProxy: process.env.HTTPS_PROXY || process.env.https_proxy,
|
|
576
613
|
noProxy: process.env.NO_PROXY || process.env.no_proxy,
|
|
@@ -582,7 +619,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
582
619
|
message: "Starting Vertex settings creation with network configuration analysis",
|
|
583
620
|
});
|
|
584
621
|
try {
|
|
585
|
-
const vertexSettings = await createVertexSettings(this.location, this.credentials);
|
|
622
|
+
const vertexSettings = await createVertexSettings(this.location, this.credentials, modelName);
|
|
586
623
|
const vertexSettingsEndTime = process.hrtime.bigint();
|
|
587
624
|
const vertexSettingsDurationNs = vertexSettingsEndTime - vertexSettingsStartTime;
|
|
588
625
|
logger.debug(`[GoogleVertexProvider] ✅ LOG_POINT_V009_VERTEX_SETTINGS_SUCCESS`, {
|
|
@@ -957,6 +994,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
957
994
|
}),
|
|
958
995
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
959
996
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
997
|
+
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
960
998
|
...(options.thinkingConfig?.enabled && {
|
|
961
999
|
providerOptions: {
|
|
962
1000
|
vertex: {
|
|
@@ -1116,12 +1154,13 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1116
1154
|
/**
|
|
1117
1155
|
* Create @google/genai client configured for Vertex AI
|
|
1118
1156
|
*/
|
|
1119
|
-
async createVertexGenAIClient(regionOverride) {
|
|
1157
|
+
async createVertexGenAIClient(regionOverride, modelName) {
|
|
1120
1158
|
const project = this.credentials?.projectId || getVertexProjectId();
|
|
1121
|
-
const
|
|
1159
|
+
const configuredLocation = this.credentials?.location ||
|
|
1122
1160
|
regionOverride ||
|
|
1123
1161
|
this.location ||
|
|
1124
1162
|
getVertexLocation();
|
|
1163
|
+
const location = resolveVertexLocation(modelName, configuredLocation);
|
|
1125
1164
|
const mod = await import("@google/genai");
|
|
1126
1165
|
const ctor = mod.GoogleGenAI;
|
|
1127
1166
|
if (!ctor) {
|
|
@@ -1308,8 +1347,8 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1308
1347
|
}, (span) => this.executeNativeGemini3StreamWithSpan(options, modelName, span));
|
|
1309
1348
|
}
|
|
1310
1349
|
async executeNativeGemini3StreamWithSpan(options, modelName, span) {
|
|
1311
|
-
const client = await this.createVertexGenAIClient(options.region);
|
|
1312
|
-
const effectiveLocation = options.region || this.location || getVertexLocation();
|
|
1350
|
+
const client = await this.createVertexGenAIClient(options.region, modelName);
|
|
1351
|
+
const effectiveLocation = resolveVertexLocation(modelName, options.region || this.location || getVertexLocation());
|
|
1313
1352
|
logger.debug("[GoogleVertex] Using native @google/genai for Gemini 3", {
|
|
1314
1353
|
model: modelName,
|
|
1315
1354
|
hasTools: !!options.tools && Object.keys(options.tools).length > 0,
|
|
@@ -1503,8 +1542,8 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1503
1542
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
1504
1543
|
},
|
|
1505
1544
|
}, async (span) => {
|
|
1506
|
-
const client = await this.createVertexGenAIClient(options.region);
|
|
1507
|
-
const effectiveLocation = options.region || this.location || getVertexLocation();
|
|
1545
|
+
const client = await this.createVertexGenAIClient(options.region, modelName);
|
|
1546
|
+
const effectiveLocation = resolveVertexLocation(modelName, options.region || this.location || getVertexLocation());
|
|
1508
1547
|
logger.debug("[GoogleVertex] Using native @google/genai for Gemini 3 generate", {
|
|
1509
1548
|
model: modelName,
|
|
1510
1549
|
project: this.projectId,
|
|
@@ -139,6 +139,7 @@ export class HuggingFaceProvider extends BaseProvider {
|
|
|
139
139
|
toolChoice: resolveToolChoice(options, (shouldUseTools ? streamOptions.tools || allTools : {}), shouldUseTools),
|
|
140
140
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
141
141
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
142
|
+
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
142
143
|
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
143
144
|
this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
|
|
144
145
|
logger.warn("[HuggingFaceProvider] Failed to store tool executions", {
|
|
@@ -169,6 +169,7 @@ export class LiteLLMProvider extends BaseProvider {
|
|
|
169
169
|
}),
|
|
170
170
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
171
171
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
172
|
+
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
172
173
|
onError: (event) => {
|
|
173
174
|
const error = event.error;
|
|
174
175
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -67,6 +67,7 @@ export class MistralProvider extends BaseProvider {
|
|
|
67
67
|
toolChoice: resolveToolChoice(options, tools, shouldUseTools),
|
|
68
68
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
69
69
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
70
|
+
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
70
71
|
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
71
72
|
this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
|
|
72
73
|
logger.warn("[MistralProvider] Failed to store tool executions", {
|
package/dist/providers/openAI.js
CHANGED
|
@@ -330,6 +330,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
330
330
|
stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
|
|
331
331
|
toolChoice: resolvedToolChoice,
|
|
332
332
|
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
|
|
333
|
+
experimental_repairToolCall: this.getToolCallRepairFn(options),
|
|
333
334
|
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
|
|
334
335
|
onStepFinish: ({ toolCalls, toolResults }) => {
|
|
335
336
|
logger.info("Tool execution completed", {
|