@juspay/neurolink 10.4.1 → 10.4.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 +386 -389
- package/dist/cli/commands/proxy.d.ts +1 -1
- package/dist/cli/commands/proxy.js +124 -56
- package/dist/cli/commands/proxyAnalyze.js +10 -1
- package/dist/lib/providers/anthropic.js +18 -1
- package/dist/lib/providers/googleAiStudio.js +18 -1
- package/dist/lib/providers/googleNativeGemini3.d.ts +19 -1
- package/dist/lib/providers/googleNativeGemini3.js +61 -4
- package/dist/lib/providers/googleVertex.d.ts +48 -0
- package/dist/lib/providers/googleVertex.js +204 -108
- package/dist/lib/providers/openaiChatCompletionsBase.js +34 -2
- package/dist/lib/providers/openaiChatCompletionsClient.d.ts +11 -5
- package/dist/lib/providers/openaiChatCompletionsClient.js +14 -8
- package/dist/lib/proxy/proxyAnalysis.js +136 -12
- package/dist/lib/proxy/requestLogger.d.ts +2 -0
- package/dist/lib/proxy/requestLogger.js +72 -13
- package/dist/lib/proxy/rollingProxyServer.js +79 -8
- package/dist/lib/proxy/rollingWorkerProcess.js +20 -15
- package/dist/lib/proxy/rollingWorkerProtocol.js +3 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.js +31 -11
- package/dist/lib/proxy/runtimeConfig.d.ts +1 -0
- package/dist/lib/proxy/runtimeConfig.js +20 -5
- package/dist/lib/proxy/socketWorkerRuntime.js +41 -13
- package/dist/lib/server/routes/claudeProxyRoutes.js +63 -75
- package/dist/lib/tools/toolDiscovery.d.ts +25 -3
- package/dist/lib/tools/toolDiscovery.js +76 -4
- package/dist/lib/types/proxy.d.ts +31 -1
- package/dist/lib/types/toolResolution.d.ts +10 -0
- package/dist/providers/anthropic.js +18 -1
- package/dist/providers/googleAiStudio.js +18 -1
- package/dist/providers/googleNativeGemini3.d.ts +19 -1
- package/dist/providers/googleNativeGemini3.js +61 -4
- package/dist/providers/googleVertex.d.ts +48 -0
- package/dist/providers/googleVertex.js +204 -108
- package/dist/providers/openaiChatCompletionsBase.js +34 -2
- package/dist/providers/openaiChatCompletionsClient.d.ts +11 -5
- package/dist/providers/openaiChatCompletionsClient.js +14 -8
- package/dist/proxy/proxyAnalysis.js +136 -12
- package/dist/proxy/requestLogger.d.ts +2 -0
- package/dist/proxy/requestLogger.js +72 -13
- package/dist/proxy/rollingProxyServer.js +79 -8
- package/dist/proxy/rollingWorkerProcess.js +20 -15
- package/dist/proxy/rollingWorkerProtocol.js +3 -0
- package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/proxy/rollingWorkerSupervisor.js +31 -11
- package/dist/proxy/runtimeConfig.d.ts +1 -0
- package/dist/proxy/runtimeConfig.js +20 -5
- package/dist/proxy/socketWorkerRuntime.js +41 -13
- package/dist/server/routes/claudeProxyRoutes.js +63 -75
- package/dist/tools/toolDiscovery.d.ts +25 -3
- package/dist/tools/toolDiscovery.js +76 -4
- package/dist/types/proxy.d.ts +31 -1
- package/dist/types/toolResolution.d.ts +10 -0
- package/package.json +1 -1
|
@@ -26,6 +26,7 @@ import { TimeoutError, raceWithAbort, withTimeout, } from "../utils/async/index.
|
|
|
26
26
|
import { parseTimeout } from "../utils/timeout.js";
|
|
27
27
|
import { appendStepText, buildAbortedTurnMessage, buildContextCapMessage, buildToolLoopCapMessage, buildTurnStalledMessage, buildTurnTimeoutMessage, buildWrapupNudgeText, createContextGuard, createTextChannel, createTurnClock, extractThoughtSignature, isAbortError, mapGeminiFinishReason, prependConversationMessages, resolveTurnStopReason, DedupExecuteMap, } from "./googleNativeGemini3.js";
|
|
28
28
|
import { getContextWindowSize } from "../constants/contextWindows.js";
|
|
29
|
+
import { resolveLiveTool } from "../tools/toolDiscovery.js";
|
|
29
30
|
import { ATTR, LANGFUSE_ATTR, spanJsonAttribute, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../telemetry/index.js";
|
|
30
31
|
import { SpanKind, SpanStatusCode, context as otelContext, trace as otelTrace, } from "@opentelemetry/api";
|
|
31
32
|
import { calculateCost } from "../utils/pricing.js";
|
|
@@ -997,6 +998,168 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
997
998
|
},
|
|
998
999
|
});
|
|
999
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Convert one AI-SDK tool into a Vertex Gemini function declaration.
|
|
1003
|
+
* Single source for the pre-loop snapshot AND the mid-turn discovery
|
|
1004
|
+
* refresh, so tools hydrated by search_tools get the exact same schema
|
|
1005
|
+
* treatment (inline, typed, additionalProperties stripped).
|
|
1006
|
+
*/
|
|
1007
|
+
buildGeminiFunctionDeclaration(name, tool) {
|
|
1008
|
+
const decl = {
|
|
1009
|
+
name,
|
|
1010
|
+
description: tool.description || `Tool: ${name}`,
|
|
1011
|
+
};
|
|
1012
|
+
// Access legacy `parameters` (AI SDK v3/v4) or current `inputSchema` (v6)
|
|
1013
|
+
const legacyTool = tool;
|
|
1014
|
+
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
1015
|
+
if (toolParams) {
|
|
1016
|
+
// Convert and inline schema to resolve $ref/definitions
|
|
1017
|
+
const rawSchema = convertZodToJsonSchema(toolParams, "openApi3");
|
|
1018
|
+
const inlinedSchema = inlineJsonSchema(rawSchema);
|
|
1019
|
+
// Remove $schema if present - @google/genai doesn't need it
|
|
1020
|
+
if (inlinedSchema.$schema) {
|
|
1021
|
+
delete inlinedSchema.$schema;
|
|
1022
|
+
}
|
|
1023
|
+
// CRITICAL: Google Vertex AI requires ALL nested schemas to have a type field
|
|
1024
|
+
// ensureNestedSchemaTypes recursively adds missing type fields to tool schemas
|
|
1025
|
+
// Note: convertZodToJsonSchema now uses openApi3 target which produces nullable: true
|
|
1026
|
+
const typedSchema = ensureNestedSchemaTypes(inlinedSchema);
|
|
1027
|
+
// Strip `additionalProperties` recursively — Vertex Gemini's
|
|
1028
|
+
// function-call validator rejects it on object schemas (returns
|
|
1029
|
+
// 400 INVALID_ARGUMENT) even though it's valid OpenAPI 3. The
|
|
1030
|
+
// field has no semantic meaning to the model, so dropping it
|
|
1031
|
+
// before send is safe for every caller.
|
|
1032
|
+
stripAdditionalPropertiesDeep(typedSchema);
|
|
1033
|
+
decl.parametersJsonSchema = typedSchema;
|
|
1034
|
+
}
|
|
1035
|
+
return decl;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Mid-turn tool sync for the native Gemini loops. `search_tools` hydrates
|
|
1039
|
+
* discovered tools into the live `options.tools` record between steps, but
|
|
1040
|
+
* the loop's declarations + executeMap are a pre-loop snapshot — without
|
|
1041
|
+
* this refresh, a tool discovered this turn stays invisible to the rest of
|
|
1042
|
+
* the turn and every call to it dies as TOOL_NOT_FOUND. Mutates the
|
|
1043
|
+
* declaration array in place (the request config holds it by reference)
|
|
1044
|
+
* and clears breaker strikes accrued while the tool was still deferred.
|
|
1045
|
+
*/
|
|
1046
|
+
refreshGeminiToolDeclarations(liveTools, declarations, executeMap, failedTools) {
|
|
1047
|
+
if (!liveTools || !declarations) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const declared = new Set(declarations.map((d) => d.name));
|
|
1051
|
+
for (const [name, tool] of Object.entries(liveTools)) {
|
|
1052
|
+
if (declared.has(name)) {
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
declarations.push(this.buildGeminiFunctionDeclaration(name, tool));
|
|
1056
|
+
if (tool.execute) {
|
|
1057
|
+
executeMap.set(name, tool.execute);
|
|
1058
|
+
}
|
|
1059
|
+
failedTools.delete(name);
|
|
1060
|
+
logger.info(`[GoogleVertex] Tool "${name}" hydrated mid-turn via discovery — added to Gemini declarations.`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Dispatch-miss recovery for the native Gemini loops: the model called a
|
|
1065
|
+
* name missing from the executeMap snapshot. Re-read the live record (a
|
|
1066
|
+
* tool hydrated by search_tools in this very step batch) or auto-hydrate a
|
|
1067
|
+
* deferred catalog tool the model called directly by its advertised name.
|
|
1068
|
+
* Returns the dedup-wrapped executor, or undefined when the name is
|
|
1069
|
+
* genuinely unknown — callers keep TOOL_NOT_FOUND for that case.
|
|
1070
|
+
*/
|
|
1071
|
+
resolveGeminiToolOnMiss(name, liveTools, declarations, executeMap, failedTools) {
|
|
1072
|
+
if (!declarations) {
|
|
1073
|
+
return undefined;
|
|
1074
|
+
}
|
|
1075
|
+
const tool = resolveLiveTool(liveTools, name);
|
|
1076
|
+
if (!tool?.execute) {
|
|
1077
|
+
return undefined;
|
|
1078
|
+
}
|
|
1079
|
+
if (!declarations.some((d) => d.name === name)) {
|
|
1080
|
+
declarations.push(this.buildGeminiFunctionDeclaration(name, tool));
|
|
1081
|
+
}
|
|
1082
|
+
executeMap.set(name, tool.execute);
|
|
1083
|
+
// NOT_FOUND strikes accrued while the tool was deferred are snapshot
|
|
1084
|
+
// artifacts, not real failures — reset so the breaker starts clean.
|
|
1085
|
+
failedTools.delete(name);
|
|
1086
|
+
logger.info(`[GoogleVertex] Tool "${name}" resolved mid-turn via discovery — executing.`);
|
|
1087
|
+
return executeMap.get(name);
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Convert one AI-SDK tool into an Anthropic (Claude-on-Vertex) tool
|
|
1091
|
+
* declaration. Single source for the pre-loop snapshot AND the mid-turn
|
|
1092
|
+
* discovery refresh — Anthropic validates input_schema as JSON Schema
|
|
1093
|
+
* draft 2020-12 and rejects OpenAPI-3 dialect (`nullable: true`), so this
|
|
1094
|
+
* uses the default JSON Schema target, matching the direct anthropic
|
|
1095
|
+
* provider. The Gemini paths keep "openApi3".
|
|
1096
|
+
*/
|
|
1097
|
+
buildAnthropicToolDeclaration(name, tool) {
|
|
1098
|
+
const anthropicTool = {
|
|
1099
|
+
name,
|
|
1100
|
+
description: tool.description || `Tool: ${name}`,
|
|
1101
|
+
input_schema: {
|
|
1102
|
+
type: "object",
|
|
1103
|
+
},
|
|
1104
|
+
};
|
|
1105
|
+
// Access legacy `parameters` (AI SDK v3/v4) or current `inputSchema` (v6)
|
|
1106
|
+
const legacyTool = tool;
|
|
1107
|
+
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
1108
|
+
if (toolParams) {
|
|
1109
|
+
const jsonSchema = convertZodToJsonSchema(toolParams);
|
|
1110
|
+
const inlined = inlineJsonSchema(jsonSchema);
|
|
1111
|
+
anthropicTool.input_schema = {
|
|
1112
|
+
type: "object",
|
|
1113
|
+
properties: inlined.properties || {},
|
|
1114
|
+
required: inlined.required || [],
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
return anthropicTool;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Mid-turn tool sync for the Claude-on-Vertex loops — the Anthropic
|
|
1121
|
+
* counterpart of refreshGeminiToolDeclarations. Claude only calls tools
|
|
1122
|
+
* present in the request's `tools` array, so without this per-step refresh
|
|
1123
|
+
* a tool discovered via search_tools is unreachable for the rest of the
|
|
1124
|
+
* turn. Mutates the array in place (requestParams holds it by reference).
|
|
1125
|
+
*/
|
|
1126
|
+
refreshAnthropicToolDeclarations(liveTools, declarations, executeMap, failedTools) {
|
|
1127
|
+
if (!liveTools || !declarations) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const declared = new Set(declarations.map((d) => d.name));
|
|
1131
|
+
for (const [name, tool] of Object.entries(liveTools)) {
|
|
1132
|
+
if (declared.has(name)) {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
declarations.push(this.buildAnthropicToolDeclaration(name, tool));
|
|
1136
|
+
if (tool.execute) {
|
|
1137
|
+
executeMap.set(name, tool.execute);
|
|
1138
|
+
}
|
|
1139
|
+
failedTools.delete(name);
|
|
1140
|
+
logger.info(`[GoogleVertex] Tool "${name}" hydrated mid-turn via discovery — added to Anthropic declarations.`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Dispatch-miss recovery for the Claude-on-Vertex loops — the Anthropic
|
|
1145
|
+
* counterpart of resolveGeminiToolOnMiss.
|
|
1146
|
+
*/
|
|
1147
|
+
resolveAnthropicToolOnMiss(name, liveTools, declarations, executeMap, failedTools) {
|
|
1148
|
+
if (!declarations) {
|
|
1149
|
+
return undefined;
|
|
1150
|
+
}
|
|
1151
|
+
const tool = resolveLiveTool(liveTools, name);
|
|
1152
|
+
if (!tool?.execute) {
|
|
1153
|
+
return undefined;
|
|
1154
|
+
}
|
|
1155
|
+
if (!declarations.some((d) => d.name === name)) {
|
|
1156
|
+
declarations.push(this.buildAnthropicToolDeclaration(name, tool));
|
|
1157
|
+
}
|
|
1158
|
+
executeMap.set(name, tool.execute);
|
|
1159
|
+
failedTools.delete(name);
|
|
1160
|
+
logger.info(`[GoogleVertex] Tool "${name}" resolved mid-turn via discovery — executing.`);
|
|
1161
|
+
return executeMap.get(name);
|
|
1162
|
+
}
|
|
1000
1163
|
/**
|
|
1001
1164
|
* Execute stream using native @google/genai SDK for Gemini 3 models on Vertex AI
|
|
1002
1165
|
* This bypasses @ai-sdk/google-vertex to properly handle thought_signature
|
|
@@ -1144,34 +1307,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1144
1307
|
!options.disableTools) {
|
|
1145
1308
|
const functionDeclarations = [];
|
|
1146
1309
|
for (const [name, tool] of Object.entries(options.tools)) {
|
|
1147
|
-
|
|
1148
|
-
name,
|
|
1149
|
-
description: tool.description || `Tool: ${name}`,
|
|
1150
|
-
};
|
|
1151
|
-
// Access legacy `parameters` (AI SDK v3/v4) or current `inputSchema` (v6)
|
|
1152
|
-
const legacyTool = tool;
|
|
1153
|
-
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
1154
|
-
if (toolParams) {
|
|
1155
|
-
// Convert and inline schema to resolve $ref/definitions
|
|
1156
|
-
const rawSchema = convertZodToJsonSchema(toolParams, "openApi3");
|
|
1157
|
-
const inlinedSchema = inlineJsonSchema(rawSchema);
|
|
1158
|
-
// Remove $schema if present - @google/genai doesn't need it
|
|
1159
|
-
if (inlinedSchema.$schema) {
|
|
1160
|
-
delete inlinedSchema.$schema;
|
|
1161
|
-
}
|
|
1162
|
-
// CRITICAL: Google Vertex AI requires ALL nested schemas to have a type field
|
|
1163
|
-
// ensureNestedSchemaTypes recursively adds missing type fields to tool schemas
|
|
1164
|
-
// Note: convertZodToJsonSchema now uses openApi3 target which produces nullable: true
|
|
1165
|
-
const typedSchema = ensureNestedSchemaTypes(inlinedSchema);
|
|
1166
|
-
// Strip `additionalProperties` recursively — Vertex Gemini's
|
|
1167
|
-
// function-call validator rejects it on object schemas (returns
|
|
1168
|
-
// 400 INVALID_ARGUMENT) even though it's valid OpenAPI 3. The
|
|
1169
|
-
// field has no semantic meaning to the model, so dropping it
|
|
1170
|
-
// before send is safe for every caller.
|
|
1171
|
-
stripAdditionalPropertiesDeep(typedSchema);
|
|
1172
|
-
decl.parametersJsonSchema = typedSchema;
|
|
1173
|
-
}
|
|
1174
|
-
functionDeclarations.push(decl);
|
|
1310
|
+
functionDeclarations.push(this.buildGeminiFunctionDeclaration(name, tool));
|
|
1175
1311
|
if (tool.execute) {
|
|
1176
1312
|
executeMap.set(name, tool.execute);
|
|
1177
1313
|
}
|
|
@@ -1390,6 +1526,11 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1390
1526
|
}
|
|
1391
1527
|
step++;
|
|
1392
1528
|
turnClock.noteProgress();
|
|
1529
|
+
// Mid-turn discovery sync: search_tools may have hydrated new tools
|
|
1530
|
+
// into the live record during the previous step — advertise them in
|
|
1531
|
+
// this step's request instead of leaving them invisible until the
|
|
1532
|
+
// next turn (TOOL_NOT_FOUND).
|
|
1533
|
+
this.refreshGeminiToolDeclarations(options.tools, tools?.[0]?.functionDeclarations, executeMap, failedTools);
|
|
1393
1534
|
logger.debug(`[GoogleVertex] Native SDK step ${step}/${maxSteps}`);
|
|
1394
1535
|
try {
|
|
1395
1536
|
const stream = await client.models.generateContentStream({
|
|
@@ -1565,7 +1706,13 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1565
1706
|
});
|
|
1566
1707
|
continue;
|
|
1567
1708
|
}
|
|
1568
|
-
|
|
1709
|
+
let execute = executeMap.get(call.name);
|
|
1710
|
+
if (!execute) {
|
|
1711
|
+
// Snapshot miss: the tool may have been hydrated into the live
|
|
1712
|
+
// record by search_tools within this very step batch, or the
|
|
1713
|
+
// model called a deferred catalog tool directly by name.
|
|
1714
|
+
execute = this.resolveGeminiToolOnMiss(call.name, options.tools, tools?.[0]?.functionDeclarations, executeMap, failedTools);
|
|
1715
|
+
}
|
|
1569
1716
|
if (execute) {
|
|
1570
1717
|
try {
|
|
1571
1718
|
// AI SDK Tool execute requires (args, options) - provide minimal options
|
|
@@ -2089,34 +2236,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2089
2236
|
if (Object.keys(combinedTools).length > 0) {
|
|
2090
2237
|
const functionDeclarations = [];
|
|
2091
2238
|
for (const [name, tool] of Object.entries(combinedTools)) {
|
|
2092
|
-
|
|
2093
|
-
name,
|
|
2094
|
-
description: tool.description || `Tool: ${name}`,
|
|
2095
|
-
};
|
|
2096
|
-
// Access legacy `parameters` (AI SDK v3/v4) or current `inputSchema` (v6)
|
|
2097
|
-
const legacyTool = tool;
|
|
2098
|
-
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
2099
|
-
if (toolParams) {
|
|
2100
|
-
// Convert and inline schema to resolve $ref/definitions
|
|
2101
|
-
const rawSchema = convertZodToJsonSchema(toolParams, "openApi3");
|
|
2102
|
-
const inlinedSchema = inlineJsonSchema(rawSchema);
|
|
2103
|
-
// Remove $schema if present - @google/genai doesn't need it
|
|
2104
|
-
if (inlinedSchema.$schema) {
|
|
2105
|
-
delete inlinedSchema.$schema;
|
|
2106
|
-
}
|
|
2107
|
-
// CRITICAL: Google Vertex AI requires ALL nested schemas to have a type field
|
|
2108
|
-
// ensureNestedSchemaTypes recursively adds missing type fields to tool schemas
|
|
2109
|
-
// Note: convertZodToJsonSchema now uses openApi3 target which produces nullable: true
|
|
2110
|
-
const typedSchema = ensureNestedSchemaTypes(inlinedSchema);
|
|
2111
|
-
// Strip `additionalProperties` recursively — Vertex Gemini's
|
|
2112
|
-
// function-call validator rejects it on object schemas (returns
|
|
2113
|
-
// 400 INVALID_ARGUMENT) even though it's valid OpenAPI 3. The
|
|
2114
|
-
// field has no semantic meaning to the model, so dropping it
|
|
2115
|
-
// before send is safe for every caller.
|
|
2116
|
-
stripAdditionalPropertiesDeep(typedSchema);
|
|
2117
|
-
decl.parametersJsonSchema = typedSchema;
|
|
2118
|
-
}
|
|
2119
|
-
functionDeclarations.push(decl);
|
|
2239
|
+
functionDeclarations.push(this.buildGeminiFunctionDeclaration(name, tool));
|
|
2120
2240
|
if (tool.execute) {
|
|
2121
2241
|
executeMap.set(name, tool.execute);
|
|
2122
2242
|
}
|
|
@@ -2324,6 +2444,8 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2324
2444
|
}
|
|
2325
2445
|
step++;
|
|
2326
2446
|
turnClock.noteProgress();
|
|
2447
|
+
// Mid-turn discovery sync — see the stream twin.
|
|
2448
|
+
this.refreshGeminiToolDeclarations(combinedTools, tools?.[0]?.functionDeclarations, executeMap, failedTools);
|
|
2327
2449
|
logger.debug(`[GoogleVertex] Native SDK generate step ${step}/${maxSteps}`);
|
|
2328
2450
|
try {
|
|
2329
2451
|
// Use generateContentStream and collect all chunks (same as GoogleAIStudio)
|
|
@@ -2495,7 +2617,11 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2495
2617
|
});
|
|
2496
2618
|
continue;
|
|
2497
2619
|
}
|
|
2498
|
-
|
|
2620
|
+
let execute = executeMap.get(call.name);
|
|
2621
|
+
if (!execute) {
|
|
2622
|
+
// Snapshot miss — see the stream twin.
|
|
2623
|
+
execute = this.resolveGeminiToolOnMiss(call.name, combinedTools, tools?.[0]?.functionDeclarations, executeMap, failedTools);
|
|
2624
|
+
}
|
|
2499
2625
|
if (execute) {
|
|
2500
2626
|
try {
|
|
2501
2627
|
// AI SDK Tool execute requires (args, options) - provide minimal options
|
|
@@ -3122,30 +3248,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
3122
3248
|
!options.disableTools) {
|
|
3123
3249
|
tools = [];
|
|
3124
3250
|
for (const [name, tool] of Object.entries(options.tools)) {
|
|
3125
|
-
|
|
3126
|
-
name,
|
|
3127
|
-
description: tool.description || `Tool: ${name}`,
|
|
3128
|
-
input_schema: {
|
|
3129
|
-
type: "object",
|
|
3130
|
-
},
|
|
3131
|
-
};
|
|
3132
|
-
// Access legacy `parameters` (AI SDK v3/v4) or current `inputSchema` (v6)
|
|
3133
|
-
const legacyTool = tool;
|
|
3134
|
-
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
3135
|
-
if (toolParams) {
|
|
3136
|
-
// Anthropic validates input_schema as JSON Schema draft 2020-12 and
|
|
3137
|
-
// rejects OpenAPI-3 dialect output (e.g. `nullable: true`) with a
|
|
3138
|
-
// 400 — use the default JSON Schema target, matching the direct
|
|
3139
|
-
// anthropic provider. The Gemini paths keep "openApi3".
|
|
3140
|
-
const jsonSchema = convertZodToJsonSchema(toolParams);
|
|
3141
|
-
const inlined = inlineJsonSchema(jsonSchema);
|
|
3142
|
-
anthropicTool.input_schema = {
|
|
3143
|
-
type: "object",
|
|
3144
|
-
properties: inlined.properties || {},
|
|
3145
|
-
required: inlined.required || [],
|
|
3146
|
-
};
|
|
3147
|
-
}
|
|
3148
|
-
tools.push(anthropicTool);
|
|
3251
|
+
tools.push(this.buildAnthropicToolDeclaration(name, tool));
|
|
3149
3252
|
if (tool.execute) {
|
|
3150
3253
|
executeMap.set(name, tool.execute);
|
|
3151
3254
|
}
|
|
@@ -3386,6 +3489,11 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
3386
3489
|
}
|
|
3387
3490
|
step++;
|
|
3388
3491
|
turnClock.noteProgress();
|
|
3492
|
+
// Mid-turn discovery sync: Claude only calls tools declared in the
|
|
3493
|
+
// request, so tools hydrated by search_tools last step must be
|
|
3494
|
+
// advertised now (requestParams.tools holds this array by
|
|
3495
|
+
// reference).
|
|
3496
|
+
this.refreshAnthropicToolDeclarations(options.tools, tools, executeMap, failedTools);
|
|
3389
3497
|
// One generation observation per API call: request in, content + usage out.
|
|
3390
3498
|
const generationSpan = tracers.generation.startSpan("anthropic.messages.stream", {
|
|
3391
3499
|
kind: SpanKind.CLIENT,
|
|
@@ -3641,7 +3749,12 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
3641
3749
|
}
|
|
3642
3750
|
toolSpan.end();
|
|
3643
3751
|
};
|
|
3644
|
-
|
|
3752
|
+
let execute = executeMap.get(toolUse.name);
|
|
3753
|
+
if (!execute) {
|
|
3754
|
+
// Snapshot miss: hydrated by search_tools this step batch, or
|
|
3755
|
+
// a deferred catalog tool called directly by name.
|
|
3756
|
+
execute = this.resolveAnthropicToolOnMiss(toolUse.name, options.tools, tools, executeMap, failedTools);
|
|
3757
|
+
}
|
|
3645
3758
|
if (execute) {
|
|
3646
3759
|
try {
|
|
3647
3760
|
const toolOptions = {
|
|
@@ -4435,30 +4548,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4435
4548
|
Object.keys(options.tools).length > 0) {
|
|
4436
4549
|
tools = [];
|
|
4437
4550
|
for (const [name, tool] of Object.entries(options.tools)) {
|
|
4438
|
-
|
|
4439
|
-
name,
|
|
4440
|
-
description: tool.description || `Tool: ${name}`,
|
|
4441
|
-
input_schema: {
|
|
4442
|
-
type: "object",
|
|
4443
|
-
},
|
|
4444
|
-
};
|
|
4445
|
-
// Access legacy `parameters` (AI SDK v3/v4) or current `inputSchema` (v6)
|
|
4446
|
-
const legacyTool = tool;
|
|
4447
|
-
const toolParams = legacyTool.parameters || tool.inputSchema;
|
|
4448
|
-
if (toolParams) {
|
|
4449
|
-
// Anthropic validates input_schema as JSON Schema draft 2020-12 and
|
|
4450
|
-
// rejects OpenAPI-3 dialect output (e.g. `nullable: true`) with a
|
|
4451
|
-
// 400 — use the default JSON Schema target, matching the direct
|
|
4452
|
-
// anthropic provider. The Gemini paths keep "openApi3".
|
|
4453
|
-
const jsonSchema = convertZodToJsonSchema(toolParams);
|
|
4454
|
-
const inlined = inlineJsonSchema(jsonSchema);
|
|
4455
|
-
anthropicTool.input_schema = {
|
|
4456
|
-
type: "object",
|
|
4457
|
-
properties: inlined.properties || {},
|
|
4458
|
-
required: inlined.required || [],
|
|
4459
|
-
};
|
|
4460
|
-
}
|
|
4461
|
-
tools.push(anthropicTool);
|
|
4551
|
+
tools.push(this.buildAnthropicToolDeclaration(name, tool));
|
|
4462
4552
|
if (tool.execute) {
|
|
4463
4553
|
executeMap.set(name, tool.execute);
|
|
4464
4554
|
}
|
|
@@ -4626,6 +4716,8 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4626
4716
|
}
|
|
4627
4717
|
step++;
|
|
4628
4718
|
turnClock.noteProgress();
|
|
4719
|
+
// Mid-turn discovery sync — see the stream twin.
|
|
4720
|
+
this.refreshAnthropicToolDeclarations(options.tools, tools, executeMap, failedTools);
|
|
4629
4721
|
try {
|
|
4630
4722
|
// Bound the SDK wait so a stalled Vertex/Anthropic call can't hang
|
|
4631
4723
|
// generate forever. options.timeout wins if set, otherwise default
|
|
@@ -4748,7 +4840,11 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4748
4840
|
});
|
|
4749
4841
|
continue;
|
|
4750
4842
|
}
|
|
4751
|
-
|
|
4843
|
+
let execute = executeMap.get(toolUse.name);
|
|
4844
|
+
if (!execute) {
|
|
4845
|
+
// Snapshot miss — see the stream twin.
|
|
4846
|
+
execute = this.resolveAnthropicToolOnMiss(toolUse.name, options.tools, tools, executeMap, failedTools);
|
|
4847
|
+
}
|
|
4752
4848
|
if (execute) {
|
|
4753
4849
|
try {
|
|
4754
4850
|
const toolOptions = {
|
|
@@ -31,6 +31,7 @@ import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals,
|
|
|
31
31
|
import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
|
|
32
32
|
import { resolveToolChoice } from "../utils/toolChoice.js";
|
|
33
33
|
import { transformToolExecutions } from "../utils/transformationUtils.js";
|
|
34
|
+
import { resolveDeferredTool } from "../tools/toolDiscovery.js";
|
|
34
35
|
import { buildAPIError, buildBody, buildToolsForOpenAI, buildWireToolNameMaps, createChunkQueue, createDeferredAnalytics, ensureJsonWordInBody, estimateWireTokens, mapNeuroLinkToolChoice, mergeUsage, messageBuilderToOpenAI, parseSSEStream, stringifyToolOutput, stripTrailingSlash, v3ResponseFormatToOpenAI, v3ToolChoiceToOpenAI, v3ToolsToOpenAI, } from "./openaiChatCompletionsClient.js";
|
|
35
36
|
/**
|
|
36
37
|
* Safety margin (tokens) when fitting `max_tokens` to a runtime-discovered
|
|
@@ -730,7 +731,34 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
730
731
|
try {
|
|
731
732
|
let stepFinish = null;
|
|
732
733
|
let stepUsage;
|
|
734
|
+
// May grow mid-turn: hydrated tools with wire-unsafe names need
|
|
735
|
+
// reverse-mapping even when the initial name set required none.
|
|
736
|
+
let effectiveToolNameFromWire = toolNameFromWire;
|
|
733
737
|
for (let step = 0; step < maxSteps; step++) {
|
|
738
|
+
// Mid-turn discovery sync: search_tools (tools.discovery) hydrates
|
|
739
|
+
// new tools into toolsRecord between steps. Dispatch already re-reads
|
|
740
|
+
// the record — this block makes the request DECLARE them so the model
|
|
741
|
+
// can call them, keeping the wire-name round-trip intact.
|
|
742
|
+
if (openAITools) {
|
|
743
|
+
const declared = new Set(openAITools.map((t) => effectiveToolNameFromWire?.get(t.function.name) ??
|
|
744
|
+
t.function.name));
|
|
745
|
+
const hydrated = Object.fromEntries(Object.entries(toolsRecord).filter(([name]) => !declared.has(name)));
|
|
746
|
+
if (Object.keys(hydrated).length > 0) {
|
|
747
|
+
// Seed with the wire names already declared this turn — mapping
|
|
748
|
+
// only the hydrated subset could otherwise re-issue a wire name
|
|
749
|
+
// an earlier tool claimed (sanitized or literal), making the
|
|
750
|
+
// reverse mapping ambiguous.
|
|
751
|
+
const extraMaps = buildWireToolNameMaps(Object.keys(hydrated), new Set(openAITools.map((t) => t.function.name)));
|
|
752
|
+
if (extraMaps) {
|
|
753
|
+
effectiveToolNameFromWire ??= new Map();
|
|
754
|
+
for (const [wireName, registryName] of extraMaps.fromWire) {
|
|
755
|
+
effectiveToolNameFromWire.set(wireName, registryName);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
openAITools.push(...(buildToolsForOpenAI(hydrated, extraMaps?.toWire) ?? []));
|
|
759
|
+
logger.info(`${this.providerName}: ${Object.keys(hydrated).length} tool(s) hydrated mid-turn via discovery: ${Object.keys(hydrated).join(", ")}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
734
762
|
const stepResult = await this.streamOneStep({
|
|
735
763
|
modelId,
|
|
736
764
|
url,
|
|
@@ -753,7 +781,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
753
781
|
stepResult,
|
|
754
782
|
conversation,
|
|
755
783
|
toolsRecord,
|
|
756
|
-
toolNameFromWire,
|
|
784
|
+
toolNameFromWire: effectiveToolNameFromWire,
|
|
757
785
|
emitter,
|
|
758
786
|
toolsUsed,
|
|
759
787
|
toolExecutionSummaries,
|
|
@@ -876,7 +904,11 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
876
904
|
// reporting use the registered names (reverse-mapped when a wire-name
|
|
877
905
|
// map is in effect — see buildWireToolNameMaps).
|
|
878
906
|
const registryName = toolNameFromWire?.get(t.name) ?? t.name;
|
|
879
|
-
|
|
907
|
+
// Live record lookup, then deferred-catalog auto-hydration: with
|
|
908
|
+
// tools.discovery on, the model may call a cataloged tool it never
|
|
909
|
+
// loaded via search_tools — that's a real tool, not a hallucination.
|
|
910
|
+
const toolDef = toolsRecord[registryName] ??
|
|
911
|
+
resolveDeferredTool(toolsRecord, registryName);
|
|
880
912
|
emitter?.emit("tool:start", {
|
|
881
913
|
toolName: registryName,
|
|
882
914
|
toolCallId: t.id,
|
|
@@ -17,12 +17,18 @@ import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatCh
|
|
|
17
17
|
export declare const stripTrailingSlash: (s: string) => string;
|
|
18
18
|
/**
|
|
19
19
|
* Build a bijective original ↔ wire tool-name map. Returns undefined when
|
|
20
|
-
* every name is already wire-valid (the common case — the
|
|
21
|
-
* original names untouched and callers skip all mapping).
|
|
22
|
-
* that collide get a deterministic numeric suffix so the
|
|
23
|
-
* invertible.
|
|
20
|
+
* every name is already wire-valid and unreserved (the common case — the
|
|
21
|
+
* wire then uses original names untouched and callers skip all mapping).
|
|
22
|
+
* Sanitized names that collide get a deterministic numeric suffix so the
|
|
23
|
+
* map stays invertible.
|
|
24
|
+
*
|
|
25
|
+
* `reservedWireNames` seeds the collision set with wire names already
|
|
26
|
+
* declared in the request — a mid-turn discovery refresh maps only the
|
|
27
|
+
* newly hydrated subset, and without seeding a hydrated name (sanitized OR
|
|
28
|
+
* wire-valid) could reuse a wire name an earlier tool already claimed,
|
|
29
|
+
* making the reverse mapping ambiguous.
|
|
24
30
|
*/
|
|
25
|
-
export declare const buildWireToolNameMaps: (names: readonly string[]) => {
|
|
31
|
+
export declare const buildWireToolNameMaps: (names: readonly string[], reservedWireNames?: ReadonlySet<string>) => {
|
|
26
32
|
toWire: Map<string, string>;
|
|
27
33
|
fromWire: Map<string, string>;
|
|
28
34
|
} | undefined;
|
|
@@ -28,27 +28,33 @@ export const stripTrailingSlash = (s) => s.replace(/\/+$/, "");
|
|
|
28
28
|
const WIRE_TOOL_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$/;
|
|
29
29
|
/**
|
|
30
30
|
* Build a bijective original ↔ wire tool-name map. Returns undefined when
|
|
31
|
-
* every name is already wire-valid (the common case — the
|
|
32
|
-
* original names untouched and callers skip all mapping).
|
|
33
|
-
* that collide get a deterministic numeric suffix so the
|
|
34
|
-
* invertible.
|
|
31
|
+
* every name is already wire-valid and unreserved (the common case — the
|
|
32
|
+
* wire then uses original names untouched and callers skip all mapping).
|
|
33
|
+
* Sanitized names that collide get a deterministic numeric suffix so the
|
|
34
|
+
* map stays invertible.
|
|
35
|
+
*
|
|
36
|
+
* `reservedWireNames` seeds the collision set with wire names already
|
|
37
|
+
* declared in the request — a mid-turn discovery refresh maps only the
|
|
38
|
+
* newly hydrated subset, and without seeding a hydrated name (sanitized OR
|
|
39
|
+
* wire-valid) could reuse a wire name an earlier tool already claimed,
|
|
40
|
+
* making the reverse mapping ambiguous.
|
|
35
41
|
*/
|
|
36
|
-
export const buildWireToolNameMaps = (names) => {
|
|
37
|
-
if (names.every((name) => WIRE_TOOL_NAME_RE.test(name))) {
|
|
42
|
+
export const buildWireToolNameMaps = (names, reservedWireNames) => {
|
|
43
|
+
if (names.every((name) => WIRE_TOOL_NAME_RE.test(name) && !reservedWireNames?.has(name))) {
|
|
38
44
|
return undefined;
|
|
39
45
|
}
|
|
40
46
|
const toWire = new Map();
|
|
41
47
|
const fromWire = new Map();
|
|
42
48
|
for (const name of names) {
|
|
43
49
|
let wire = WIRE_TOOL_NAME_RE.test(name) ? name : sanitizeToolName(name);
|
|
44
|
-
if (fromWire.has(wire)) {
|
|
50
|
+
if (fromWire.has(wire) || reservedWireNames?.has(wire)) {
|
|
45
51
|
let suffix = 2;
|
|
46
52
|
let candidate;
|
|
47
53
|
do {
|
|
48
54
|
const tail = `_${suffix}`;
|
|
49
55
|
candidate = `${wire.slice(0, 64 - tail.length)}${tail}`;
|
|
50
56
|
suffix++;
|
|
51
|
-
} while (fromWire.has(candidate));
|
|
57
|
+
} while (fromWire.has(candidate) || reservedWireNames?.has(candidate));
|
|
52
58
|
wire = candidate;
|
|
53
59
|
}
|
|
54
60
|
toWire.set(name, wire);
|