@juspay/neurolink 10.4.0 → 10.4.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 (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +382 -382
  3. package/dist/core/modules/GenerationHandler.d.ts +24 -0
  4. package/dist/core/modules/GenerationHandler.js +18 -3
  5. package/dist/lib/core/modules/GenerationHandler.d.ts +24 -0
  6. package/dist/lib/core/modules/GenerationHandler.js +18 -3
  7. package/dist/lib/providers/anthropic.js +18 -1
  8. package/dist/lib/providers/googleAiStudio.js +18 -1
  9. package/dist/lib/providers/googleNativeGemini3.d.ts +19 -1
  10. package/dist/lib/providers/googleNativeGemini3.js +61 -4
  11. package/dist/lib/providers/googleVertex.d.ts +48 -0
  12. package/dist/lib/providers/googleVertex.js +204 -108
  13. package/dist/lib/providers/openaiChatCompletionsBase.js +34 -2
  14. package/dist/lib/providers/openaiChatCompletionsClient.d.ts +11 -5
  15. package/dist/lib/providers/openaiChatCompletionsClient.js +14 -8
  16. package/dist/lib/tools/toolDiscovery.d.ts +25 -3
  17. package/dist/lib/tools/toolDiscovery.js +76 -4
  18. package/dist/lib/types/toolResolution.d.ts +10 -0
  19. package/dist/providers/anthropic.js +18 -1
  20. package/dist/providers/googleAiStudio.js +18 -1
  21. package/dist/providers/googleNativeGemini3.d.ts +19 -1
  22. package/dist/providers/googleNativeGemini3.js +61 -4
  23. package/dist/providers/googleVertex.d.ts +48 -0
  24. package/dist/providers/googleVertex.js +204 -108
  25. package/dist/providers/openaiChatCompletionsBase.js +34 -2
  26. package/dist/providers/openaiChatCompletionsClient.d.ts +11 -5
  27. package/dist/providers/openaiChatCompletionsClient.js +14 -8
  28. package/dist/tools/toolDiscovery.d.ts +25 -3
  29. package/dist/tools/toolDiscovery.js +76 -4
  30. package/dist/types/toolResolution.d.ts +10 -0
  31. 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
- const decl = {
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
- const execute = executeMap.get(call.name);
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
- const decl = {
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
- const execute = executeMap.get(call.name);
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
- const anthropicTool = {
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
- const execute = executeMap.get(toolUse.name);
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
- const anthropicTool = {
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
- const execute = executeMap.get(toolUse.name);
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
- const toolDef = toolsRecord[registryName];
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 wire then uses
21
- * original names untouched and callers skip all mapping). Sanitized names
22
- * that collide get a deterministic numeric suffix so the map stays
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 wire then uses
32
- * original names untouched and callers skip all mapping). Sanitized names
33
- * that collide get a deterministic numeric suffix so the map stays
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);
@@ -6,12 +6,19 @@
6
6
  * requested tools, and previously discovered tools) plus ONE `search_tools`
7
7
  * meta-tool whose description embeds a compact name+summary catalog of the
8
8
  * deferred tools. Calling `search_tools` loads matching tools:
9
- * - immediately into the live tool record (providers that re-read the tools
10
- * record between agent-loop steps the AI SDK path — can call them on the
11
- * very next step), and
9
+ * - immediately into the live tool record the AI SDK path re-reads the
10
+ * record between agent-loop steps, and the native loops (Vertex
11
+ * Gemini/Claude, AI Studio, chat-completions, direct Anthropic) re-resolve
12
+ * against it on a dispatch miss and refresh their wire declarations per
13
+ * step — so discovered tools are callable on the very next step, and
12
14
  * - into the session pin set (all providers include them in full on every
13
15
  * subsequent call of the session).
14
16
  *
17
+ * The record additionally carries a symbol-keyed {@link DeferredToolResolver}
18
+ * (see `resolveDeferredTool`) so a loop can hydrate a cataloged tool the
19
+ * model called directly by name WITHOUT a prior search_tools call — the
20
+ * catalog advertises exact names, so models legitimately do this.
21
+ *
15
22
  * Evidence for this design: catalogs past ~30-50 tools measurably degrade
16
23
  * tool-selection accuracy, and deferral+search both cuts definition tokens by
17
24
  * ~85% and RAISES selection accuracy (Anthropic MCP-eval 49%→74% on Opus 4).
@@ -46,3 +53,18 @@ export declare function partitionToolsForDiscovery(tools: Record<string, Tool>,
46
53
  /** Called with newly discovered names — persists session pins. */
47
54
  onHydrate: (names: string[]) => void;
48
55
  }): Record<string, Tool>;
56
+ /**
57
+ * Hydrate-and-return a deferred tool by name, when `record` was produced by
58
+ * `partitionToolsForDiscovery` and `name` is in its deferred catalog.
59
+ * Returns `undefined` for records without a resolver (discovery off) and for
60
+ * names that are genuinely unknown — callers keep their hallucinated-name
61
+ * handling for that case.
62
+ */
63
+ export declare function resolveDeferredTool(record: Record<string, Tool> | undefined, name: string): Tool | undefined;
64
+ /**
65
+ * Live tool lookup for native agent loops on a dispatch miss: first re-reads
66
+ * the record (a tool hydrated by search_tools after the loop built its
67
+ * pre-step snapshot), then falls back to auto-hydrating from the deferred
68
+ * catalog. Returns `undefined` only when the name is genuinely unknown.
69
+ */
70
+ export declare function resolveLiveTool(record: Record<string, Tool> | undefined, name: string): Tool | undefined;