@juspay/neurolink 12.14.8 → 12.14.9

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/dist/index.d.ts CHANGED
@@ -46,6 +46,7 @@ export { createApiKeyAuthInterceptor, createBearerAuthInterceptor, createDynamic
46
46
  export { SSEClient, WebSocketStreamingClient, createStreamingClient, createAsyncStream, collectStream, } from "./client/streamingClient.js";
47
47
  export { OAuth2TokenManager, JWTTokenManager, createApiKeyMiddleware, createBearerTokenMiddleware, createTokenManagerMiddleware, createAuthWithRetryMiddleware, createMultiAuthMiddleware, OAuth2Error, OAuth2AuthenticationError as OAuth2AuthError, TokenRefreshError, decodeJWTPayload, isJWTExpired, getJWTExpiry, getApiKeyFromEnv, } from "./client/auth.js";
48
48
  export { ErrorCode as ClientErrorCode, NeuroLinkError as ClientNeuroLinkError, HttpError, ClientRateLimitError, ClientValidationError, ClientAuthenticationError, ClientAuthorizationError, NotFoundError, ClientNetworkError, ClientTimeoutError, ClientConnectionError, AbortError, ClientConfigurationError, StreamError, ClientProviderError, ContextLengthError, ContentFilterError, createErrorFromResponse, createErrorFromNative, mapStatusToErrorCode, isRetryableStatus, isRetryableError, isNeuroLinkError, isApiError, } from "./client/errors.js";
49
+ export { BaseError, ProviderError, AuthenticationError, AuthorizationError, NetworkError, RateLimitError, InvalidModelError, } from "./types/index.js";
49
50
  export { AIProviderName, BedrockModels, OpenAIModels, VertexModels, } from "./constants/enums.js";
50
51
  export { dynamicModelProvider } from "./core/dynamicModels.js";
51
52
  export { validateTool } from "./sdk/toolRegistration.js";
package/dist/index.js CHANGED
@@ -73,6 +73,17 @@ OAuth2TokenManager, JWTTokenManager, createApiKeyMiddleware, createBearerTokenMi
73
73
  export {
74
74
  // Errors
75
75
  ErrorCode as ClientErrorCode, NeuroLinkError as ClientNeuroLinkError, HttpError, ClientRateLimitError, ClientValidationError, ClientAuthenticationError, ClientAuthorizationError, NotFoundError, ClientNetworkError, ClientTimeoutError, ClientConnectionError, AbortError, ClientConfigurationError, StreamError, ClientProviderError, ContextLengthError, ContentFilterError, createErrorFromResponse, createErrorFromNative, mapStatusToErrorCode, isRetryableStatus, isRetryableError, isNeuroLinkError, isApiError, } from "./client/errors.js";
76
+ // The SDK-side error hierarchy `classifyProviderError()` actually throws
77
+ // (as opposed to the unrelated `Client*` REST-client errors above). These
78
+ // already reach consumers transitively through `export * from
79
+ // "./types/index.js"` below, but that makes them public by accident of the
80
+ // types barrel's shape — a future reshuffle of that barrel could drop them
81
+ // silently. Exporting them here, explicitly, by name, makes the guarantee
82
+ // intentional: `catch (e) { if (e instanceof RateLimitError) ... }` against
83
+ // `@juspay/neurolink` is a supported, stable pattern, not an implementation
84
+ // detail a caller happened to be able to reach. Imported from the barrel
85
+ // (never the specific `types/errors.js` file) per Critical Rule 13.
86
+ export { BaseError, ProviderError, AuthenticationError, AuthorizationError, NetworkError, RateLimitError, InvalidModelError, } from "./types/index.js";
76
87
  export { AIProviderName, BedrockModels, OpenAIModels, VertexModels, } from "./constants/enums.js";
77
88
  // Dynamic Models exports
78
89
  export { dynamicModelProvider } from "./core/dynamicModels.js";
@@ -185,41 +185,54 @@ const _benchmarkProviderPerformanceTool = {
185
185
  ];
186
186
  const benchmarkResults = [];
187
187
  for (const providerName of providersToTest) {
188
- const provider = await AIProviderFactory.createProvider(providerName);
189
- if (!provider) {
188
+ try {
189
+ const provider = await AIProviderFactory.createProvider(providerName);
190
+ if (!provider) {
191
+ benchmarkResults.push({
192
+ provider: providerName,
193
+ error: "Failed to create provider.",
194
+ });
195
+ continue;
196
+ }
197
+ let totalLatency = 0, totalTokens = 0, successfulTests = 0;
198
+ for (const prompt of testPrompts) {
199
+ for (let i = 0; i < typedParams.iterations; i++) {
200
+ const testStartTime = Date.now();
201
+ const result = await provider.generate({
202
+ prompt: prompt,
203
+ maxTokens: typedParams.maxTokens,
204
+ });
205
+ if (result && result.usage) {
206
+ totalLatency += Date.now() - testStartTime;
207
+ totalTokens += result.usage.total || 0;
208
+ successfulTests++;
209
+ }
210
+ }
211
+ }
190
212
  benchmarkResults.push({
191
213
  provider: providerName,
192
- error: "Failed to create provider.",
214
+ metrics: {
215
+ avgLatency: successfulTests > 0
216
+ ? Math.round(totalLatency / successfulTests)
217
+ : 0,
218
+ total: totalTokens,
219
+ successRate: (successfulTests /
220
+ (testPrompts.length * typedParams.iterations)) *
221
+ 100,
222
+ },
193
223
  });
194
- continue;
195
224
  }
196
- let totalLatency = 0, totalTokens = 0, successfulTests = 0;
197
- for (const prompt of testPrompts) {
198
- for (let i = 0; i < typedParams.iterations; i++) {
199
- const testStartTime = Date.now();
200
- const result = await provider.generate({
201
- prompt: prompt,
202
- maxTokens: typedParams.maxTokens,
203
- });
204
- if (result && result.usage) {
205
- totalLatency += Date.now() - testStartTime;
206
- totalTokens += result.usage.total || 0;
207
- successfulTests++;
208
- }
209
- }
225
+ catch (providerError) {
226
+ // A single provider's failure (network, auth, rate-limit, etc.)
227
+ // must not abort the whole benchmark — record it and keep testing
228
+ // the remaining providers, mirroring the null-provider branch above.
229
+ benchmarkResults.push({
230
+ provider: providerName,
231
+ error: providerError instanceof Error
232
+ ? providerError.message
233
+ : String(providerError),
234
+ });
210
235
  }
211
- benchmarkResults.push({
212
- provider: providerName,
213
- metrics: {
214
- avgLatency: successfulTests > 0
215
- ? Math.round(totalLatency / successfulTests)
216
- : 0,
217
- total: totalTokens,
218
- successRate: (successfulTests /
219
- (testPrompts.length * typedParams.iterations)) *
220
- 100,
221
- },
222
- });
223
236
  }
224
237
  const executionTime = Date.now() - startTime;
225
238
  return {
@@ -1648,7 +1648,10 @@ export declare class NeuroLink {
1648
1648
  */
1649
1649
  testProvider(providerName: string): Promise<boolean>;
1650
1650
  /**
1651
- * Internal method to test provider connection with minimal generation call
1651
+ * Internal method to test provider connection with minimal generation call.
1652
+ * `signal` lets a caller (getProviderStatus's per-provider race) cancel the
1653
+ * underlying request once it has already lost the race against its
1654
+ * timeout, instead of leaving it running unobserved.
1652
1655
  */
1653
1656
  private testProviderConnection;
1654
1657
  /**
package/dist/neurolink.js CHANGED
@@ -6490,6 +6490,14 @@ Current user's request: ${currentInput}`;
6490
6490
  if (lastError instanceof ContextBudgetExceededError) {
6491
6491
  throw lastError;
6492
6492
  }
6493
+ // A single-candidate list (an explicit provider, or
6494
+ // disableInternalFallback) never actually tried "all providers" —
6495
+ // naming the one provider that failed is clearer than the generic
6496
+ // multi-provider wording. The multi-provider prefix is left untouched
6497
+ // so existing message-matching suites keep passing.
6498
+ if (providersToTry.length === 1) {
6499
+ throw new Error(`Provider ${providersToTry[0]} failed: ${lastError?.message || "Unknown error"}`);
6500
+ }
6493
6501
  throw new Error(`Failed to generate text with all providers. Last error: ${lastError?.message || "Unknown error"}`);
6494
6502
  }
6495
6503
  /**
@@ -10863,13 +10871,36 @@ Current user's request: ${currentInput}`;
10863
10871
  };
10864
10872
  }
10865
10873
  }
10866
- // Test other providers with actual generation call
10874
+ // Test other providers with actual generation call. The losing
10875
+ // side of the race is aborted in `finally` below — previously the
10876
+ // timeout promise alone rejected while the still-running
10877
+ // testProviderConnection() call (and the provider request inside
10878
+ // it) kept executing to completion unobserved, wasting quota and
10879
+ // holding the connection open past the 5s status check.
10867
10880
  const testTimeout = 5000;
10868
- const testPromise = this.testProviderConnection(providerName);
10881
+ const abortController = new AbortController();
10882
+ const testPromise = this.testProviderConnection(providerName, abortController.signal);
10883
+ let timeoutHandle;
10869
10884
  const timeoutPromise = new Promise((_, reject) => {
10870
- setTimeout(() => reject(new Error("Provider test timeout (5s)")), testTimeout);
10885
+ timeoutHandle = setTimeout(() => {
10886
+ reject(new Error("Provider test timeout (5s)"));
10887
+ }, testTimeout);
10871
10888
  });
10872
- await Promise.race([testPromise, timeoutPromise]);
10889
+ try {
10890
+ await Promise.race([testPromise, timeoutPromise]);
10891
+ }
10892
+ finally {
10893
+ // Runs on both outcomes: clears the timer so it can't fire after
10894
+ // a fast success, and aborts the signal so a timed-out
10895
+ // generate() call stops instead of finishing unobserved.
10896
+ clearTimeout(timeoutHandle);
10897
+ // The aborted generate() resolves/rejects after the race is
10898
+ // decided; swallow that settlement so it can't surface as an
10899
+ // unhandledRejection, mirroring raceWithAbort's handling of the
10900
+ // losing side.
10901
+ testPromise.catch(() => undefined);
10902
+ abortController.abort();
10903
+ }
10873
10904
  return {
10874
10905
  provider: providerName,
10875
10906
  status: "working",
@@ -10919,15 +10950,19 @@ Current user's request: ${currentInput}`;
10919
10950
  }
10920
10951
  }
10921
10952
  /**
10922
- * Internal method to test provider connection with minimal generation call
10953
+ * Internal method to test provider connection with minimal generation call.
10954
+ * `signal` lets a caller (getProviderStatus's per-provider race) cancel the
10955
+ * underlying request once it has already lost the race against its
10956
+ * timeout, instead of leaving it running unobserved.
10923
10957
  */
10924
- async testProviderConnection(providerName) {
10958
+ async testProviderConnection(providerName, signal) {
10925
10959
  const { AIProviderFactory } = await import("./core/factory.js");
10926
10960
  const provider = await AIProviderFactory.createProvider(providerName, null);
10927
10961
  await provider.generate({
10928
10962
  prompt: "test",
10929
10963
  maxTokens: 1,
10930
10964
  disableTools: true,
10965
+ abortSignal: signal,
10931
10966
  });
10932
10967
  }
10933
10968
  /**
@@ -172,7 +172,7 @@ export class AzureOpenAIProvider extends OpenAIChatCompletionsProvider {
172
172
  },
173
173
  ...DEFAULT_ERROR_RULES,
174
174
  ];
175
- return classifyProviderError(error, rules, "azure", this.modelName);
175
+ return classifyProviderError(error, rules, this.providerName, this.modelName);
176
176
  }
177
177
  // ===========================================================================
178
178
  // New overridable hooks (provided by the base-enhancement branch)
@@ -2,5 +2,5 @@ import type { OpenAICompatCatalogEntry, ProviderCatalogJson, ProviderConfigOptio
2
2
  export declare function catalogCredentialsKey(entry: ProviderCatalogJson): string;
3
3
  export declare function catalogEnvVar(entry: ProviderCatalogJson, kind: "apiKey" | "baseURL" | "model"): string;
4
4
  export declare function buildCatalogConfigOptions(entry: ProviderCatalogJson): ProviderConfigOptions;
5
- export declare function getCatalogJsonEntries(): ProviderCatalogJson[];
5
+ export declare function getCatalogJsonEntries(): readonly ProviderCatalogJson[];
6
6
  export declare function buildCatalogEntries(): OpenAICompatCatalogEntry[];
@@ -70,8 +70,37 @@ function buildErrorRules(entry) {
70
70
  });
71
71
  return [...bespoke, ...DEFAULT_ERROR_RULES];
72
72
  }
73
+ // Recursively freezes a value in place and returns it. Used only on a
74
+ // structuredClone()d graph below — never on CATALOG_JSON_ENTRIES itself —
75
+ // so this never touches the module-level singleton.
76
+ function deepFreeze(value) {
77
+ if (Array.isArray(value)) {
78
+ for (const item of value) {
79
+ deepFreeze(item);
80
+ }
81
+ return Object.freeze(value);
82
+ }
83
+ if (value !== null && typeof value === "object") {
84
+ for (const key of Object.keys(value)) {
85
+ deepFreeze(value[key]);
86
+ }
87
+ return Object.freeze(value);
88
+ }
89
+ return value;
90
+ }
91
+ // Returns a deep clone of the catalog graph, frozen at every level, never
92
+ // the module-level singleton by reference or anything sharing its nested
93
+ // objects/arrays — CATALOG_JSON_ENTRIES is reused on every call, so handing
94
+ // out (a shallow copy of) the live array would let any one caller's
95
+ // mutation corrupt catalog state for the rest of the process, and freezing
96
+ // only the outer array left `entry.aliases`, `entry.models.fallbacks`,
97
+ // `entry.models.catalog` etc. shared and mutable despite the `readonly`
98
+ // return type. structuredClone() first (so the frozen graph never aliases
99
+ // CATALOG_JSON_ENTRIES's own objects), then deepFreeze() the clone. All
100
+ // current callers only .find/.map/.flatMap/.filter/iterate, so this is
101
+ // behavior-preserving for them.
73
102
  export function getCatalogJsonEntries() {
74
- return CATALOG_JSON_ENTRIES;
103
+ return deepFreeze(structuredClone(CATALOG_JSON_ENTRIES));
75
104
  }
76
105
  export function buildCatalogEntries() {
77
106
  return CATALOG_JSON_ENTRIES.map((entry) => {
@@ -87,7 +87,7 @@ export class CohereProvider extends OpenAIChatCompletionsProvider {
87
87
  },
88
88
  ...DEFAULT_ERROR_RULES,
89
89
  ];
90
- return classifyProviderError(error, rules, "cohere", this.modelName);
90
+ return classifyProviderError(error, rules, this.providerName, this.modelName);
91
91
  }
92
92
  async validateConfiguration() {
93
93
  return (typeof this.config.apiKey === "string" &&
@@ -89,7 +89,7 @@ export class DeepSeekProvider extends OpenAIChatCompletionsProvider {
89
89
  },
90
90
  ...DEFAULT_ERROR_RULES,
91
91
  ];
92
- return classifyProviderError(error, rules, "deepseek", this.modelName);
92
+ return classifyProviderError(error, rules, this.providerName, this.modelName);
93
93
  }
94
94
  // ===========================================================================
95
95
  // Optional hooks — provider-specific quirks
@@ -1289,12 +1289,13 @@ export class GoogleAIStudioProvider extends BaseProvider {
1289
1289
  ...options,
1290
1290
  tools: baseTools,
1291
1291
  };
1292
- // Check for tools + JSON schema conflict (Gemini limitation)
1292
+ // Check for tools + JSON schema conflict, via the shared predicate (same
1293
+ // gate executeStream() and executeNativeGemini3Generate() use) so this
1294
+ // path also honors isGeminiProvider/isNativeAnthropicProvider instead of
1295
+ // hand-rolling its own tools+schema condition.
1293
1296
  const wantsJsonOutput = options.output?.format === "json" || options.schema;
1294
- if (wantsJsonOutput &&
1295
- mergedOptions.tools &&
1296
- Object.keys(mergedOptions.tools).length > 0 &&
1297
- !mergedOptions.disableTools) {
1297
+ const exclusionInForce = isToolsSchemaExclusionInForce(this.providerName, modelName, !mergedOptions.disableTools, Object.keys(mergedOptions.tools ?? {}).length);
1298
+ if (wantsJsonOutput && exclusionInForce) {
1298
1299
  logger.warn("[GoogleAIStudio] Gemini does not support tools and JSON schema output simultaneously. Disabling tools for this request.");
1299
1300
  mergedOptions = { ...mergedOptions, disableTools: true, tools: {} };
1300
1301
  }
@@ -138,32 +138,6 @@ export declare class GoogleVertexProvider extends BaseProvider {
138
138
  * this purpose.
139
139
  */
140
140
  private resolveExpressApiKey;
141
- /**
142
- * Convert one AI-SDK tool into a Vertex Gemini function declaration.
143
- * Single source for the pre-loop snapshot AND the mid-turn discovery
144
- * refresh, so tools hydrated by search_tools get the exact same schema
145
- * treatment (inline, typed, additionalProperties stripped).
146
- */
147
- private buildGeminiFunctionDeclaration;
148
- /**
149
- * Mid-turn tool sync for the native Gemini loops. `search_tools` hydrates
150
- * discovered tools into the live `options.tools` record between steps, but
151
- * the loop's declarations + executeMap are a pre-loop snapshot — without
152
- * this refresh, a tool discovered this turn stays invisible to the rest of
153
- * the turn and every call to it dies as TOOL_NOT_FOUND. Mutates the
154
- * declaration array in place (the request config holds it by reference)
155
- * and clears breaker strikes accrued while the tool was still deferred.
156
- */
157
- private refreshGeminiToolDeclarations;
158
- /**
159
- * Dispatch-miss recovery for the native Gemini loops: the model called a
160
- * name missing from the executeMap snapshot. Re-read the live record (a
161
- * tool hydrated by search_tools in this very step batch) or auto-hydrate a
162
- * deferred catalog tool the model called directly by its advertised name.
163
- * Returns the dedup-wrapped executor, or undefined when the name is
164
- * genuinely unknown — callers keep TOOL_NOT_FOUND for that case.
165
- */
166
- private resolveGeminiToolOnMiss;
167
141
  /**
168
142
  * Convert one AI-SDK tool into an Anthropic (Claude-on-Vertex) tool
169
143
  * declaration. Single source for the pre-loop snapshot AND the mid-turn
@@ -174,18 +148,25 @@ export declare class GoogleVertexProvider extends BaseProvider {
174
148
  */
175
149
  private buildAnthropicToolDeclaration;
176
150
  /**
177
- * Mid-turn tool sync for the Claude-on-Vertex loops — the Anthropic
178
- * counterpart of refreshGeminiToolDeclarations. Claude only calls tools
151
+ * Mid-turn tool sync for the Claude-on-Vertex loops. Claude only calls tools
179
152
  * present in the request's `tools` array, so without this per-step refresh
180
153
  * a tool discovered via search_tools is unreachable for the rest of the
181
154
  * turn. Mutates the array in place (requestParams holds it by reference).
182
155
  */
183
156
  private refreshAnthropicToolDeclarations;
184
157
  /**
185
- * Dispatch-miss recovery for the Claude-on-Vertex loops — the Anthropic
186
- * counterpart of resolveGeminiToolOnMiss.
158
+ * Dispatch-miss recovery for the Claude-on-Vertex loops.
187
159
  */
188
160
  private resolveAnthropicToolOnMiss;
161
+ /**
162
+ * Build the synthetic `final_result` tool used to force structured output
163
+ * on Claude-on-Vertex. Anthropic has no native responseSchema, so a schema
164
+ * is instead enforced by requiring the model to call this tool with
165
+ * matching arguments. Single source for the non-streaming and streaming
166
+ * paths — both previously built this object independently from their own
167
+ * schema variable, byte-identical apart from that variable's name.
168
+ */
169
+ private buildFinalResultTool;
189
170
  /**
190
171
  * Execute stream using native @google/genai SDK for Gemini 3 models on Vertex AI
191
172
  * This bypasses @ai-sdk/google-vertex to properly handle thought_signature
@@ -977,9 +977,13 @@ export class GoogleVertexProvider extends BaseProvider {
977
977
  async createVertexGenAIClient(regionOverride) {
978
978
  warnGoogleSdkIgnoresProxy("GoogleVertex");
979
979
  const expressApiKey = this.resolveExpressApiKey();
980
- // Resolved only on the ADC path: getVertexProjectId() throws when no
981
- // project is configured, which an Express request legitimately has none of.
982
- const project = expressApiKey ? "" : getVertexProjectId();
980
+ // Resolved only on the ADC path, and from the per-instance projectId the
981
+ // constructor already settled (credentials.projectId if supplied, else
982
+ // the same ambient getVertexProjectId() fallback) — not re-read from
983
+ // ambient env here, or a per-request projectId override would be built
984
+ // into the client with the wrong project while call sites elsewhere
985
+ // (generateContent's `project:` param) correctly used this.projectId.
986
+ const project = expressApiKey ? "" : this.projectId;
983
987
  const location = regionOverride || this.location || getVertexLocation();
984
988
  const mod = await import("@google/genai");
985
989
  const ctor = mod.GoogleGenAI;
@@ -1060,94 +1064,6 @@ export class GoogleVertexProvider extends BaseProvider {
1060
1064
  ].some((name) => (process.env[name] ?? "").trim().length > 0);
1061
1065
  return projectConfigured ? undefined : fromEnv;
1062
1066
  }
1063
- /**
1064
- * Convert one AI-SDK tool into a Vertex Gemini function declaration.
1065
- * Single source for the pre-loop snapshot AND the mid-turn discovery
1066
- * refresh, so tools hydrated by search_tools get the exact same schema
1067
- * treatment (inline, typed, additionalProperties stripped).
1068
- */
1069
- buildGeminiFunctionDeclaration(name, tool) {
1070
- const decl = {
1071
- name,
1072
- description: tool.description || `Tool: ${name}`,
1073
- };
1074
- // Access legacy `parameters` (AI SDK v3/v4) or current `inputSchema` (v6)
1075
- const legacyTool = tool;
1076
- const toolParams = legacyTool.parameters || tool.inputSchema;
1077
- if (toolParams) {
1078
- // Convert and inline schema to resolve $ref/definitions
1079
- const rawSchema = convertZodToJsonSchema(toolParams, "openApi3");
1080
- const inlinedSchema = inlineJsonSchema(rawSchema);
1081
- // Remove $schema if present - @google/genai doesn't need it
1082
- if (inlinedSchema.$schema) {
1083
- delete inlinedSchema.$schema;
1084
- }
1085
- // CRITICAL: Google Vertex AI requires ALL nested schemas to have a type field
1086
- // ensureNestedSchemaTypes recursively adds missing type fields to tool schemas
1087
- // Note: convertZodToJsonSchema now uses openApi3 target which produces nullable: true
1088
- const typedSchema = ensureNestedSchemaTypes(inlinedSchema);
1089
- // Strip `additionalProperties` recursively — Vertex Gemini's
1090
- // function-call validator rejects it on object schemas (returns
1091
- // 400 INVALID_ARGUMENT) even though it's valid OpenAPI 3. The
1092
- // field has no semantic meaning to the model, so dropping it
1093
- // before send is safe for every caller.
1094
- stripAdditionalPropertiesDeep(typedSchema);
1095
- decl.parametersJsonSchema = typedSchema;
1096
- }
1097
- return decl;
1098
- }
1099
- /**
1100
- * Mid-turn tool sync for the native Gemini loops. `search_tools` hydrates
1101
- * discovered tools into the live `options.tools` record between steps, but
1102
- * the loop's declarations + executeMap are a pre-loop snapshot — without
1103
- * this refresh, a tool discovered this turn stays invisible to the rest of
1104
- * the turn and every call to it dies as TOOL_NOT_FOUND. Mutates the
1105
- * declaration array in place (the request config holds it by reference)
1106
- * and clears breaker strikes accrued while the tool was still deferred.
1107
- */
1108
- refreshGeminiToolDeclarations(liveTools, declarations, executeMap, failedTools) {
1109
- if (!liveTools || !declarations) {
1110
- return;
1111
- }
1112
- const declared = new Set(declarations.map((d) => d.name));
1113
- for (const [name, tool] of Object.entries(liveTools)) {
1114
- if (declared.has(name)) {
1115
- continue;
1116
- }
1117
- declarations.push(this.buildGeminiFunctionDeclaration(name, tool));
1118
- if (tool.execute) {
1119
- executeMap.set(name, tool.execute);
1120
- }
1121
- failedTools.delete(name);
1122
- logger.info(`[GoogleVertex] Tool "${name}" hydrated mid-turn via discovery — added to Gemini declarations.`);
1123
- }
1124
- }
1125
- /**
1126
- * Dispatch-miss recovery for the native Gemini loops: the model called a
1127
- * name missing from the executeMap snapshot. Re-read the live record (a
1128
- * tool hydrated by search_tools in this very step batch) or auto-hydrate a
1129
- * deferred catalog tool the model called directly by its advertised name.
1130
- * Returns the dedup-wrapped executor, or undefined when the name is
1131
- * genuinely unknown — callers keep TOOL_NOT_FOUND for that case.
1132
- */
1133
- resolveGeminiToolOnMiss(name, liveTools, declarations, executeMap, failedTools) {
1134
- if (!declarations) {
1135
- return undefined;
1136
- }
1137
- const tool = resolveLiveTool(liveTools, name);
1138
- if (!tool?.execute) {
1139
- return undefined;
1140
- }
1141
- if (!declarations.some((d) => d.name === name)) {
1142
- declarations.push(this.buildGeminiFunctionDeclaration(name, tool));
1143
- }
1144
- executeMap.set(name, tool.execute);
1145
- // NOT_FOUND strikes accrued while the tool was deferred are snapshot
1146
- // artifacts, not real failures — reset so the breaker starts clean.
1147
- failedTools.delete(name);
1148
- logger.info(`[GoogleVertex] Tool "${name}" resolved mid-turn via discovery — executing.`);
1149
- return executeMap.get(name);
1150
- }
1151
1067
  /**
1152
1068
  * Convert one AI-SDK tool into an Anthropic (Claude-on-Vertex) tool
1153
1069
  * declaration. Single source for the pre-loop snapshot AND the mid-turn
@@ -1179,8 +1095,7 @@ export class GoogleVertexProvider extends BaseProvider {
1179
1095
  return anthropicTool;
1180
1096
  }
1181
1097
  /**
1182
- * Mid-turn tool sync for the Claude-on-Vertex loops — the Anthropic
1183
- * counterpart of refreshGeminiToolDeclarations. Claude only calls tools
1098
+ * Mid-turn tool sync for the Claude-on-Vertex loops. Claude only calls tools
1184
1099
  * present in the request's `tools` array, so without this per-step refresh
1185
1100
  * a tool discovered via search_tools is unreachable for the rest of the
1186
1101
  * turn. Mutates the array in place (requestParams holds it by reference).
@@ -1203,8 +1118,7 @@ export class GoogleVertexProvider extends BaseProvider {
1203
1118
  }
1204
1119
  }
1205
1120
  /**
1206
- * Dispatch-miss recovery for the Claude-on-Vertex loops — the Anthropic
1207
- * counterpart of resolveGeminiToolOnMiss.
1121
+ * Dispatch-miss recovery for the Claude-on-Vertex loops.
1208
1122
  */
1209
1123
  resolveAnthropicToolOnMiss(name, liveTools, declarations, executeMap, failedTools) {
1210
1124
  if (!declarations) {
@@ -1222,6 +1136,31 @@ export class GoogleVertexProvider extends BaseProvider {
1222
1136
  logger.info(`[GoogleVertex] Tool "${name}" resolved mid-turn via discovery — executing.`);
1223
1137
  return executeMap.get(name);
1224
1138
  }
1139
+ /**
1140
+ * Build the synthetic `final_result` tool used to force structured output
1141
+ * on Claude-on-Vertex. Anthropic has no native responseSchema, so a schema
1142
+ * is instead enforced by requiring the model to call this tool with
1143
+ * matching arguments. Single source for the non-streaming and streaming
1144
+ * paths — both previously built this object independently from their own
1145
+ * schema variable, byte-identical apart from that variable's name.
1146
+ */
1147
+ buildFinalResultTool(schema) {
1148
+ const schemaAsJson = convertZodToJsonSchema(schema);
1149
+ const inlinedSchema = inlineJsonSchema(schemaAsJson);
1150
+ if (inlinedSchema.$schema) {
1151
+ delete inlinedSchema.$schema;
1152
+ }
1153
+ const typedSchema = ensureNestedSchemaTypes(inlinedSchema);
1154
+ return {
1155
+ name: "final_result",
1156
+ description: "Return the final structured result. You MUST call this tool when you have gathered all information and are ready to provide the final answer. The arguments should contain the structured data matching the expected schema.",
1157
+ input_schema: {
1158
+ type: "object",
1159
+ properties: typedSchema.properties || typedSchema,
1160
+ required: typedSchema.required || [],
1161
+ },
1162
+ };
1163
+ }
1225
1164
  /**
1226
1165
  * Execute stream using native @google/genai SDK for Gemini 3 models on Vertex AI
1227
1166
  * This bypasses @ai-sdk/google-vertex to properly handle thought_signature
@@ -3132,23 +3071,8 @@ export class GoogleVertexProvider extends BaseProvider {
3132
3071
  let schemaSystemPromptSuffix = "";
3133
3072
  if (streamOptions.schema) {
3134
3073
  useFinalResultTool = true;
3135
- // Convert schema to JSON schema format
3136
- const schemaAsJson = convertZodToJsonSchema(streamOptions.schema);
3137
- const inlinedSchema = inlineJsonSchema(schemaAsJson);
3138
- if (inlinedSchema.$schema) {
3139
- delete inlinedSchema.$schema;
3140
- }
3141
- const typedSchema = ensureNestedSchemaTypes(inlinedSchema);
3142
3074
  // Create final_result tool
3143
- const finalResultTool = {
3144
- name: "final_result",
3145
- description: "Return the final structured result. You MUST call this tool when you have gathered all information and are ready to provide the final answer. The arguments should contain the structured data matching the expected schema.",
3146
- input_schema: {
3147
- type: "object",
3148
- properties: typedSchema.properties || typedSchema,
3149
- required: typedSchema.required || [],
3150
- },
3151
- };
3075
+ const finalResultTool = this.buildFinalResultTool(streamOptions.schema);
3152
3076
  // Add to tools array or create new array
3153
3077
  if (!tools) {
3154
3078
  tools = [];
@@ -3158,7 +3082,7 @@ export class GoogleVertexProvider extends BaseProvider {
3158
3082
  schemaSystemPromptSuffix =
3159
3083
  "\n\nIMPORTANT: You MUST call the 'final_result' tool to return your response in the required structured format. Do not respond with plain text - always use the final_result tool.";
3160
3084
  logger.debug("[GoogleVertex] Added final_result tool for Anthropic structured output (stream)", {
3161
- schemaKeys: Object.keys(typedSchema),
3085
+ schemaKeys: Object.keys(finalResultTool.input_schema.properties ?? {}),
3162
3086
  totalTools: tools.length,
3163
3087
  });
3164
3088
  }
@@ -3719,10 +3643,19 @@ export class GoogleVertexProvider extends BaseProvider {
3719
3643
  // with text in step 2 would look unfinished and fall into terminal
3720
3644
  // handling. The finish reason is the per-turn signal — the engine
3721
3645
  // reports "tool-calls" only when the cap was hit with tools still
3722
- // pending.
3646
+ // pending. That signal is not enough on its own, though: Anthropic
3647
+ // reports stop_reason "tool_use" — which maps to finishReason
3648
+ // "tool-calls" — for ANY tool_use block, including a genuine
3649
+ // terminal `final_result` call. Without the third clause, a turn
3650
+ // that called a real tool earlier (toolCalls.length > 0) and then
3651
+ // legitimately finished via final_result reads as unfinished and
3652
+ // falls into the terminal-handling block below, which forces a
3653
+ // redundant second final_result call and pushes its JSON onto the
3654
+ // stream a second time.
3723
3655
  modelFinished =
3724
3656
  engineResult.toolCalls.length === 0 ||
3725
- engineResult.finishReason !== "tool-calls";
3657
+ engineResult.finishReason !== "tool-calls" ||
3658
+ (useFinalResultTool && structuredOutputRef.value !== undefined);
3726
3659
  // Replace in place: the terminal block and the finalization call both
3727
3660
  // read `currentMessages`.
3728
3661
  currentMessages.length = 0;
@@ -4361,23 +4294,8 @@ export class GoogleVertexProvider extends BaseProvider {
4361
4294
  let schemaSystemPromptSuffix = "";
4362
4295
  if (options.schema) {
4363
4296
  useFinalResultTool = true;
4364
- // Convert schema to JSON schema format
4365
- const schemaAsJson = convertZodToJsonSchema(options.schema);
4366
- const inlinedSchema = inlineJsonSchema(schemaAsJson);
4367
- if (inlinedSchema.$schema) {
4368
- delete inlinedSchema.$schema;
4369
- }
4370
- const typedSchema = ensureNestedSchemaTypes(inlinedSchema);
4371
4297
  // Create final_result tool
4372
- const finalResultTool = {
4373
- name: "final_result",
4374
- description: "Return the final structured result. You MUST call this tool when you have gathered all information and are ready to provide the final answer. The arguments should contain the structured data matching the expected schema.",
4375
- input_schema: {
4376
- type: "object",
4377
- properties: typedSchema.properties || typedSchema,
4378
- required: typedSchema.required || [],
4379
- },
4380
- };
4298
+ const finalResultTool = this.buildFinalResultTool(options.schema);
4381
4299
  // Add to tools array or create new array
4382
4300
  if (!tools) {
4383
4301
  tools = [];
@@ -4387,7 +4305,7 @@ export class GoogleVertexProvider extends BaseProvider {
4387
4305
  schemaSystemPromptSuffix =
4388
4306
  "\n\nIMPORTANT: You MUST call the 'final_result' tool to return your response in the required structured format. Do not respond with plain text - always use the final_result tool.";
4389
4307
  logger.debug("[GoogleVertex] Added final_result tool for Anthropic structured output (generate)", {
4390
- schemaKeys: Object.keys(typedSchema),
4308
+ schemaKeys: Object.keys(finalResultTool.input_schema.properties ?? {}),
4391
4309
  totalTools: tools.length,
4392
4310
  });
4393
4311
  }
@@ -97,6 +97,6 @@ export class HuggingFaceProvider extends OpenAIChatCompletionsProvider {
97
97
  },
98
98
  ...DEFAULT_ERROR_RULES,
99
99
  ];
100
- return classifyProviderError(error, rules, "huggingface", this.modelName);
100
+ return classifyProviderError(error, rules, this.providerName, this.modelName);
101
101
  }
102
102
  }
@@ -74,6 +74,6 @@ export class LlamaCppProvider extends OpenAIChatCompletionsProvider {
74
74
  },
75
75
  ...DEFAULT_ERROR_RULES,
76
76
  ];
77
- return classifyProviderError(error, rules, "llamacpp", this.modelName);
77
+ return classifyProviderError(error, rules, this.providerName, this.modelName);
78
78
  }
79
79
  }