@juspay/neurolink 9.80.1 → 9.80.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.
- package/CHANGELOG.md +6 -0
- package/dist/browser/neurolink.min.js +349 -349
- package/dist/core/constants.d.ts +2 -0
- package/dist/core/constants.js +2 -0
- package/dist/lib/core/constants.d.ts +2 -0
- package/dist/lib/core/constants.js +2 -0
- package/dist/lib/providers/googleNativeGemini3.d.ts +20 -2
- package/dist/lib/providers/googleNativeGemini3.js +37 -4
- package/dist/lib/providers/googleVertex.d.ts +2 -1
- package/dist/lib/providers/googleVertex.js +626 -480
- package/dist/providers/googleNativeGemini3.d.ts +20 -2
- package/dist/providers/googleNativeGemini3.js +37 -4
- package/dist/providers/googleVertex.d.ts +2 -1
- package/dist/providers/googleVertex.js +626 -480
- package/package.json +2 -1
|
@@ -5,7 +5,7 @@ import path from "path";
|
|
|
5
5
|
import os from "os";
|
|
6
6
|
import { AIProviderName, ErrorCategory, ErrorSeverity, } from "../constants/enums.js";
|
|
7
7
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
8
|
-
import { DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, GLOBAL_LOCATION_MODELS, IMAGE_GENERATION_MODELS, TOOL_STORAGE_TIMEOUT_MS, } from "../core/constants.js";
|
|
8
|
+
import { DEFAULT_GEMINI_STREAM_TIMEOUT_MS, DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, GLOBAL_LOCATION_MODELS, IMAGE_GENERATION_MODELS, TOOL_STORAGE_TIMEOUT_MS, } from "../core/constants.js";
|
|
9
9
|
import { ModelConfigurationManager } from "../core/modelConfiguration.js";
|
|
10
10
|
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
11
11
|
import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
@@ -22,7 +22,7 @@ import { convertZodToJsonSchema, inlineJsonSchema, ensureNestedSchemaTypes, } fr
|
|
|
22
22
|
import { createNativeThinkingConfig } from "../utils/thinkingConfig.js";
|
|
23
23
|
import { TimeoutError, withTimeout } from "../utils/async/index.js";
|
|
24
24
|
import { parseTimeout } from "../utils/timeout.js";
|
|
25
|
-
import { appendStepText, createTextChannel, extractThoughtSignature, mapGeminiFinishReason, prependConversationMessages, } from "./googleNativeGemini3.js";
|
|
25
|
+
import { appendStepText, buildToolLoopCapMessage, createTextChannel, extractThoughtSignature, isAbortError, mapGeminiFinishReason, prependConversationMessages, } from "./googleNativeGemini3.js";
|
|
26
26
|
import { ATTR, LANGFUSE_ATTR, spanJsonAttribute, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../telemetry/index.js";
|
|
27
27
|
import { SpanKind, SpanStatusCode, context as otelContext, trace as otelTrace, } from "@opentelemetry/api";
|
|
28
28
|
import { calculateCost } from "../utils/pricing.js";
|
|
@@ -1321,177 +1321,238 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1321
1321
|
// yielding it once breaks that signal even though the underlying
|
|
1322
1322
|
// network stream is genuinely incremental.
|
|
1323
1323
|
const incrementalTextChunks = [];
|
|
1324
|
-
//
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1324
|
+
// Abort scaffolding (mirrors executeNativeAnthropicStream). The native
|
|
1325
|
+
// Gemini SDK cancels via config.abortSignal, so drive an internal
|
|
1326
|
+
// AbortController: the caller's signal and a defensive wall-clock timer
|
|
1327
|
+
// both trip it, and every request/tool-exec receives effectiveSignal.
|
|
1328
|
+
const streamTimeoutMs = parseTimeout(options.timeout) ?? DEFAULT_GEMINI_STREAM_TIMEOUT_MS;
|
|
1329
|
+
const internalAbort = new AbortController();
|
|
1330
|
+
const onCallerAbort = () => internalAbort.abort();
|
|
1331
|
+
options.abortSignal?.addEventListener("abort", onCallerAbort);
|
|
1332
|
+
const defensiveTimer = setTimeout(() => {
|
|
1333
|
+
logger.warn(`[GoogleVertex] Native Gemini turn exceeded ${streamTimeoutMs}ms — aborting`);
|
|
1334
|
+
internalAbort.abort();
|
|
1335
|
+
}, streamTimeoutMs);
|
|
1336
|
+
const effectiveSignal = internalAbort.signal;
|
|
1337
|
+
if (options.abortSignal?.aborted) {
|
|
1338
|
+
internalAbort.abort();
|
|
1339
|
+
}
|
|
1340
|
+
let wasAborted = false;
|
|
1341
|
+
// Step-cap flags declared in the outer scope so the terminal block (also
|
|
1342
|
+
// inside the try) and the finishReason mapping (after the finally) can
|
|
1343
|
+
// both read them.
|
|
1344
|
+
let hitStepLimit = false;
|
|
1345
|
+
let synthesizedFinalAnswer = false;
|
|
1346
|
+
try {
|
|
1347
|
+
// Agentic loop for tool calling
|
|
1348
|
+
while (step < maxSteps) {
|
|
1349
|
+
if (effectiveSignal.aborted) {
|
|
1350
|
+
wasAborted = true;
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
step++;
|
|
1354
|
+
logger.debug(`[GoogleVertex] Native SDK step ${step}/${maxSteps}`);
|
|
1355
|
+
try {
|
|
1356
|
+
const stream = await client.models.generateContentStream({
|
|
1357
|
+
model: modelName,
|
|
1358
|
+
contents: currentContents,
|
|
1359
|
+
config: { ...config, abortSignal: effectiveSignal },
|
|
1360
|
+
});
|
|
1361
|
+
const stepFunctionCalls = [];
|
|
1362
|
+
// Capture raw response parts including thoughtSignature
|
|
1363
|
+
const rawResponseParts = [];
|
|
1364
|
+
for await (const chunk of stream) {
|
|
1365
|
+
// Extract raw parts from candidates FIRST
|
|
1366
|
+
// This avoids using chunk.text which triggers SDK warning when
|
|
1367
|
+
// non-text parts (thoughtSignature, functionCall) are present
|
|
1368
|
+
const chunkRecord = chunk;
|
|
1369
|
+
const candidates = chunkRecord.candidates;
|
|
1370
|
+
const firstCandidate = candidates?.[0];
|
|
1371
|
+
// Capture the SDK finish reason (Bug 2: previously dropped). Last
|
|
1372
|
+
// non-empty value across chunks wins.
|
|
1373
|
+
const chunkFinishReason = firstCandidate?.finishReason;
|
|
1374
|
+
if (typeof chunkFinishReason === "string" && chunkFinishReason) {
|
|
1375
|
+
lastFinishReason = chunkFinishReason;
|
|
1376
|
+
}
|
|
1377
|
+
const chunkContent = firstCandidate?.content;
|
|
1378
|
+
if (chunkContent && Array.isArray(chunkContent.parts)) {
|
|
1379
|
+
for (const part of chunkContent.parts) {
|
|
1380
|
+
rawResponseParts.push(part);
|
|
1381
|
+
if (typeof part.text === "string" && part.text.length > 0) {
|
|
1382
|
+
incrementalTextChunks.push(part.text);
|
|
1383
|
+
}
|
|
1356
1384
|
}
|
|
1357
1385
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
stepFunctionCalls.push(...chunk.functionCalls);
|
|
1361
|
-
}
|
|
1362
|
-
// Extract usage metadata from chunk
|
|
1363
|
-
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
1364
|
-
const usageMetadata = chunkRecord.usageMetadata;
|
|
1365
|
-
if (usageMetadata) {
|
|
1366
|
-
// Take the latest promptTokenCount (usually only in final chunk)
|
|
1367
|
-
if (usageMetadata.promptTokenCount !== undefined &&
|
|
1368
|
-
usageMetadata.promptTokenCount > 0) {
|
|
1369
|
-
totalInputTokens = usageMetadata.promptTokenCount;
|
|
1386
|
+
if (chunk.functionCalls) {
|
|
1387
|
+
stepFunctionCalls.push(...chunk.functionCalls);
|
|
1370
1388
|
}
|
|
1371
|
-
//
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1389
|
+
// Extract usage metadata from chunk
|
|
1390
|
+
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
1391
|
+
const usageMetadata = chunkRecord.usageMetadata;
|
|
1392
|
+
if (usageMetadata) {
|
|
1393
|
+
// Take the latest promptTokenCount (usually only in final chunk)
|
|
1394
|
+
if (usageMetadata.promptTokenCount !== undefined &&
|
|
1395
|
+
usageMetadata.promptTokenCount > 0) {
|
|
1396
|
+
totalInputTokens = usageMetadata.promptTokenCount;
|
|
1397
|
+
}
|
|
1398
|
+
// Take the latest candidatesTokenCount (accumulates through chunks)
|
|
1399
|
+
if (usageMetadata.candidatesTokenCount !== undefined &&
|
|
1400
|
+
usageMetadata.candidatesTokenCount > 0) {
|
|
1401
|
+
totalOutputTokens = usageMetadata.candidatesTokenCount;
|
|
1402
|
+
}
|
|
1375
1403
|
}
|
|
1376
1404
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
finalText = stepText;
|
|
1387
|
-
break;
|
|
1388
|
-
}
|
|
1389
|
-
// Check for final_result tool call - this is our structured output pattern
|
|
1390
|
-
if (useFinalResultTool) {
|
|
1391
|
-
const finalResultCall = stepFunctionCalls.find((call) => call.name === "final_result");
|
|
1392
|
-
if (finalResultCall) {
|
|
1393
|
-
// Extract the structured output from final_result arguments
|
|
1394
|
-
finalResultStructuredOutput = finalResultCall.args;
|
|
1395
|
-
logger.debug("[GoogleVertex] Received final_result tool call with structured output (stream)", {
|
|
1396
|
-
outputKeys: Object.keys(finalResultStructuredOutput),
|
|
1397
|
-
});
|
|
1398
|
-
// Return the structured output as JSON text
|
|
1399
|
-
finalText = JSON.stringify(finalResultStructuredOutput);
|
|
1405
|
+
// Extract text from raw parts after stream completes
|
|
1406
|
+
// This avoids SDK warning about non-text parts (thoughtSignature, functionCall)
|
|
1407
|
+
const stepText = rawResponseParts
|
|
1408
|
+
.filter((part) => typeof part.text === "string")
|
|
1409
|
+
.map((part) => part.text)
|
|
1410
|
+
.join("");
|
|
1411
|
+
// If no function calls, we're done
|
|
1412
|
+
if (stepFunctionCalls.length === 0) {
|
|
1413
|
+
finalText = stepText;
|
|
1400
1414
|
break;
|
|
1401
1415
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
// Execute each function and collect responses
|
|
1416
|
-
const functionResponses = [];
|
|
1417
|
-
// Per-step bookkeeping for conversation-memory storage.
|
|
1418
|
-
const stepStorageCalls = [];
|
|
1419
|
-
const stepStorageResults = [];
|
|
1420
|
-
// Note: tool:start / tool:end events are emitted by ToolsManager's
|
|
1421
|
-
// wrapped `execute` (see ToolsManager.ts:355) — no inline emit needed.
|
|
1422
|
-
for (const call of stepFunctionCalls) {
|
|
1423
|
-
allToolCalls.push({ toolName: call.name, args: call.args });
|
|
1424
|
-
stepStorageCalls.push({ toolName: call.name, args: call.args });
|
|
1425
|
-
// Check if this tool has already exceeded retry limit
|
|
1426
|
-
const failedInfo = failedTools.get(call.name);
|
|
1427
|
-
if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
|
|
1428
|
-
logger.warn(`[GoogleVertex] Tool "${call.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
|
|
1429
|
-
const errorPayload = {
|
|
1430
|
-
error: `TOOL_PERMANENTLY_FAILED: The tool "${call.name}" has failed ${failedInfo.count} times and will not be retried. Last error: ${failedInfo.lastError}. Please proceed without using this tool or inform the user that this functionality is unavailable.`,
|
|
1431
|
-
status: "permanently_failed",
|
|
1432
|
-
do_not_retry: true,
|
|
1433
|
-
};
|
|
1434
|
-
functionResponses.push({
|
|
1435
|
-
functionResponse: {
|
|
1436
|
-
name: call.name,
|
|
1437
|
-
response: errorPayload,
|
|
1438
|
-
},
|
|
1439
|
-
});
|
|
1440
|
-
toolExecutions.push({
|
|
1441
|
-
name: call.name,
|
|
1442
|
-
input: call.args,
|
|
1443
|
-
output: errorPayload,
|
|
1444
|
-
});
|
|
1445
|
-
stepStorageResults.push({
|
|
1446
|
-
toolName: call.name,
|
|
1447
|
-
output: errorPayload,
|
|
1448
|
-
});
|
|
1449
|
-
continue;
|
|
1416
|
+
// Check for final_result tool call - this is our structured output pattern
|
|
1417
|
+
if (useFinalResultTool) {
|
|
1418
|
+
const finalResultCall = stepFunctionCalls.find((call) => call.name === "final_result");
|
|
1419
|
+
if (finalResultCall) {
|
|
1420
|
+
// Extract the structured output from final_result arguments
|
|
1421
|
+
finalResultStructuredOutput = finalResultCall.args;
|
|
1422
|
+
logger.debug("[GoogleVertex] Received final_result tool call with structured output (stream)", {
|
|
1423
|
+
outputKeys: Object.keys(finalResultStructuredOutput),
|
|
1424
|
+
});
|
|
1425
|
+
// Return the structured output as JSON text
|
|
1426
|
+
finalText = JSON.stringify(finalResultStructuredOutput);
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1450
1429
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1430
|
+
// Execute function calls
|
|
1431
|
+
logger.debug(`[GoogleVertex] Executing ${stepFunctionCalls.length} function calls`);
|
|
1432
|
+
// Add model response with ALL parts (including thoughtSignature) to history
|
|
1433
|
+
// This preserves the thought_signature which is required for Gemini 3 multi-turn tool calling
|
|
1434
|
+
currentContents.push({
|
|
1435
|
+
role: "model",
|
|
1436
|
+
parts: rawResponseParts.length > 0
|
|
1437
|
+
? rawResponseParts
|
|
1438
|
+
: stepFunctionCalls.map((fc) => ({
|
|
1439
|
+
functionCall: fc,
|
|
1440
|
+
})),
|
|
1441
|
+
});
|
|
1442
|
+
// Execute each function and collect responses
|
|
1443
|
+
const functionResponses = [];
|
|
1444
|
+
// Per-step bookkeeping for conversation-memory storage.
|
|
1445
|
+
const stepStorageCalls = [];
|
|
1446
|
+
const stepStorageResults = [];
|
|
1447
|
+
// Note: tool:start / tool:end events are emitted by ToolsManager's
|
|
1448
|
+
// wrapped `execute` (see ToolsManager.ts:355) — no inline emit needed.
|
|
1449
|
+
for (const call of stepFunctionCalls) {
|
|
1450
|
+
allToolCalls.push({ toolName: call.name, args: call.args });
|
|
1451
|
+
stepStorageCalls.push({ toolName: call.name, args: call.args });
|
|
1452
|
+
// Check if this tool has already exceeded retry limit
|
|
1453
|
+
const failedInfo = failedTools.get(call.name);
|
|
1454
|
+
if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
|
|
1455
|
+
logger.warn(`[GoogleVertex] Tool "${call.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
|
|
1456
|
+
const errorPayload = {
|
|
1457
|
+
error: `TOOL_PERMANENTLY_FAILED: The tool "${call.name}" has failed ${failedInfo.count} times and will not be retried. Last error: ${failedInfo.lastError}. Please proceed without using this tool or inform the user that this functionality is unavailable.`,
|
|
1458
|
+
status: "permanently_failed",
|
|
1459
|
+
do_not_retry: true,
|
|
1459
1460
|
};
|
|
1460
|
-
|
|
1461
|
+
functionResponses.push({
|
|
1462
|
+
functionResponse: {
|
|
1463
|
+
name: call.name,
|
|
1464
|
+
response: errorPayload,
|
|
1465
|
+
},
|
|
1466
|
+
});
|
|
1461
1467
|
toolExecutions.push({
|
|
1462
1468
|
name: call.name,
|
|
1463
1469
|
input: call.args,
|
|
1464
|
-
output:
|
|
1465
|
-
});
|
|
1466
|
-
functionResponses.push({
|
|
1467
|
-
functionResponse: { name: call.name, response: { result } },
|
|
1470
|
+
output: errorPayload,
|
|
1468
1471
|
});
|
|
1469
1472
|
stepStorageResults.push({
|
|
1470
1473
|
toolName: call.name,
|
|
1471
|
-
output:
|
|
1474
|
+
output: errorPayload,
|
|
1472
1475
|
});
|
|
1476
|
+
continue;
|
|
1473
1477
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1478
|
+
const execute = executeMap.get(call.name);
|
|
1479
|
+
if (execute) {
|
|
1480
|
+
try {
|
|
1481
|
+
// AI SDK Tool execute requires (args, options) - provide minimal options
|
|
1482
|
+
const toolOptions = {
|
|
1483
|
+
toolCallId: `${call.name}-${Date.now()}`,
|
|
1484
|
+
messages: [],
|
|
1485
|
+
abortSignal: effectiveSignal,
|
|
1486
|
+
};
|
|
1487
|
+
const result = await execute(call.args, toolOptions);
|
|
1488
|
+
toolExecutions.push({
|
|
1489
|
+
name: call.name,
|
|
1490
|
+
input: call.args,
|
|
1491
|
+
output: result,
|
|
1492
|
+
});
|
|
1493
|
+
functionResponses.push({
|
|
1494
|
+
functionResponse: { name: call.name, response: { result } },
|
|
1495
|
+
});
|
|
1496
|
+
stepStorageResults.push({
|
|
1497
|
+
toolName: call.name,
|
|
1498
|
+
output: result,
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
catch (error) {
|
|
1502
|
+
// An abort during tool execution ends the turn gracefully — it
|
|
1503
|
+
// must NOT be recorded as a spurious tool failure. Both checks
|
|
1504
|
+
// matter: effectiveSignal.aborted catches an abort WE triggered
|
|
1505
|
+
// (even if the throw isn't abort-shaped); isAbortError(error)
|
|
1506
|
+
// catches an abort-shaped throw (e.g. a tool raising AbortError)
|
|
1507
|
+
// even when the signal itself never fired.
|
|
1508
|
+
if (effectiveSignal.aborted || isAbortError(error)) {
|
|
1509
|
+
wasAborted = true;
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1513
|
+
// Track this failure
|
|
1514
|
+
const currentFailInfo = failedTools.get(call.name) || {
|
|
1515
|
+
count: 0,
|
|
1516
|
+
lastError: "",
|
|
1517
|
+
};
|
|
1518
|
+
currentFailInfo.count++;
|
|
1519
|
+
currentFailInfo.lastError = errorMessage;
|
|
1520
|
+
failedTools.set(call.name, currentFailInfo);
|
|
1521
|
+
logger.warn(`[GoogleVertex] Tool "${call.name}" failed (attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}): ${errorMessage}`);
|
|
1522
|
+
// Determine if this is a permanent failure
|
|
1523
|
+
const isPermanentFailure = currentFailInfo.count >= DEFAULT_TOOL_MAX_RETRIES;
|
|
1524
|
+
const errorPayload = {
|
|
1525
|
+
error: isPermanentFailure
|
|
1526
|
+
? `TOOL_PERMANENTLY_FAILED: The tool "${call.name}" has failed ${currentFailInfo.count} times with error: ${errorMessage}. This tool will not be retried. Please proceed without using this tool or inform the user that this functionality is unavailable.`
|
|
1527
|
+
: `TOOL_EXECUTION_ERROR: ${errorMessage}. Retry attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}.`,
|
|
1528
|
+
status: isPermanentFailure ? "permanently_failed" : "failed",
|
|
1529
|
+
do_not_retry: isPermanentFailure,
|
|
1530
|
+
retry_count: currentFailInfo.count,
|
|
1531
|
+
max_retries: DEFAULT_TOOL_MAX_RETRIES,
|
|
1532
|
+
};
|
|
1533
|
+
functionResponses.push({
|
|
1534
|
+
functionResponse: {
|
|
1535
|
+
name: call.name,
|
|
1536
|
+
response: errorPayload,
|
|
1537
|
+
},
|
|
1538
|
+
});
|
|
1539
|
+
toolExecutions.push({
|
|
1540
|
+
name: call.name,
|
|
1541
|
+
input: call.args,
|
|
1542
|
+
output: errorPayload,
|
|
1543
|
+
});
|
|
1544
|
+
stepStorageResults.push({
|
|
1545
|
+
toolName: call.name,
|
|
1546
|
+
output: errorPayload,
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
// Tool not found is a permanent error
|
|
1487
1552
|
const errorPayload = {
|
|
1488
|
-
error:
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
status: isPermanentFailure ? "permanently_failed" : "failed",
|
|
1492
|
-
do_not_retry: isPermanentFailure,
|
|
1493
|
-
retry_count: currentFailInfo.count,
|
|
1494
|
-
max_retries: DEFAULT_TOOL_MAX_RETRIES,
|
|
1553
|
+
error: `TOOL_NOT_FOUND: The tool "${call.name}" does not exist. Do not attempt to call this tool again.`,
|
|
1554
|
+
status: "permanently_failed",
|
|
1555
|
+
do_not_retry: true,
|
|
1495
1556
|
};
|
|
1496
1557
|
functionResponses.push({
|
|
1497
1558
|
functionResponse: {
|
|
@@ -1510,102 +1571,109 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1510
1571
|
});
|
|
1511
1572
|
}
|
|
1512
1573
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
do_not_retry: true,
|
|
1519
|
-
};
|
|
1520
|
-
functionResponses.push({
|
|
1521
|
-
functionResponse: {
|
|
1522
|
-
name: call.name,
|
|
1523
|
-
response: errorPayload,
|
|
1524
|
-
},
|
|
1525
|
-
});
|
|
1526
|
-
toolExecutions.push({
|
|
1527
|
-
name: call.name,
|
|
1528
|
-
input: call.args,
|
|
1529
|
-
output: errorPayload,
|
|
1530
|
-
});
|
|
1531
|
-
stepStorageResults.push({
|
|
1532
|
-
toolName: call.name,
|
|
1533
|
-
output: errorPayload,
|
|
1534
|
-
});
|
|
1574
|
+
// An abort inside the tool-exec loop only breaks that inner for-loop.
|
|
1575
|
+
// Break the while too so no further model call is issued and control
|
|
1576
|
+
// reaches the terminal step-cap handling below.
|
|
1577
|
+
if (wasAborted) {
|
|
1578
|
+
break;
|
|
1535
1579
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
:
|
|
1551
|
-
stepIndex: step,
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1580
|
+
// Persist this step's tool calls/results into conversation memory.
|
|
1581
|
+
// Without this, tool_call / tool_result rows never reach Redis and
|
|
1582
|
+
// the chat-history UI loses every tool invocation.
|
|
1583
|
+
//
|
|
1584
|
+
// `thoughtSignature` rides as a sibling on the first call of the
|
|
1585
|
+
// step — Gemini 3 needs it to match thinking patterns when the
|
|
1586
|
+
// conversation is replayed on the next turn.
|
|
1587
|
+
if (stepStorageCalls.length > 0 || stepStorageResults.length > 0) {
|
|
1588
|
+
const stepThoughtSig = extractThoughtSignature(rawResponseParts);
|
|
1589
|
+
withTimeout(this.handleToolExecutionStorage(stepStorageCalls.map((c, i) => ({
|
|
1590
|
+
...c,
|
|
1591
|
+
...(i === 0 && stepThoughtSig
|
|
1592
|
+
? { thoughtSignature: stepThoughtSig }
|
|
1593
|
+
: {}),
|
|
1594
|
+
stepIndex: step,
|
|
1595
|
+
})), stepStorageResults.map((r) => ({ ...r, stepIndex: step })), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
|
|
1596
|
+
logger.warn("[GoogleVertex] Failed to store native Gemini stream tool executions", {
|
|
1597
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1598
|
+
});
|
|
1555
1599
|
});
|
|
1600
|
+
}
|
|
1601
|
+
// The @google/genai SDK only accepts "user" and "model" as valid
|
|
1602
|
+
// roles in contents — function/tool responses must use role: "user"
|
|
1603
|
+
// (matching the SDK's automaticFunctionCalling implementation and
|
|
1604
|
+
// the Google AI Studio path). Sending role: "function" was causing
|
|
1605
|
+
// native Vertex Gemini tool loops to be silently rejected by the
|
|
1606
|
+
// request validator.
|
|
1607
|
+
currentContents.push({
|
|
1608
|
+
role: "user",
|
|
1609
|
+
parts: functionResponses,
|
|
1556
1610
|
});
|
|
1557
1611
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
}
|
|
1569
|
-
catch (error) {
|
|
1570
|
-
logger.error("[GoogleVertex] Native SDK error", error);
|
|
1571
|
-
throw this.handleProviderError(error);
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
// Handle maxSteps termination — the loop exited because the step cap was
|
|
1575
|
-
// reached while the model was still calling tools. Surface a real answer
|
|
1576
|
-
// instead of the canned placeholder (Bug 1) and a meaningful finishReason
|
|
1577
|
-
// (Bug 2).
|
|
1578
|
-
let hitStepLimit = false;
|
|
1579
|
-
let synthesizedFinalAnswer = false;
|
|
1580
|
-
if (step >= maxSteps && !finalText) {
|
|
1581
|
-
hitStepLimit = true;
|
|
1582
|
-
// The consumer receives text via `incrementalTextChunks`; any text the
|
|
1583
|
-
// model emitted across steps is already preserved there. Only synthesize
|
|
1584
|
-
// when NOTHING was produced (the pure-functionCall case that otherwise
|
|
1585
|
-
// surfaces the placeholder) so we never waste a round-trip whose output
|
|
1586
|
-
// the stream would ignore.
|
|
1587
|
-
if (incrementalTextChunks.length === 0) {
|
|
1588
|
-
logger.warn(`[GoogleVertex] Tool call loop terminated after reaching maxSteps (${maxSteps}) ` +
|
|
1589
|
-
`with no text; synthesizing a final answer with tools disabled.`);
|
|
1590
|
-
const synth = await this.synthesizeFinalAnswerWithoutTools(client, modelName, config, currentContents, useFinalResultTool, parseTimeout(options.timeout) ?? 300_000);
|
|
1591
|
-
if (synth.text) {
|
|
1592
|
-
synthesizedFinalAnswer = true;
|
|
1593
|
-
finalText = synth.text;
|
|
1594
|
-
incrementalTextChunks.push(synth.text);
|
|
1595
|
-
totalInputTokens += synth.inputTokens;
|
|
1596
|
-
totalOutputTokens += synth.outputTokens;
|
|
1597
|
-
if (synth.finishReason) {
|
|
1598
|
-
lastFinishReason = synth.finishReason;
|
|
1612
|
+
catch (error) {
|
|
1613
|
+
// A mid-drain abort surfaces as an AbortError from the `for await`.
|
|
1614
|
+
// Break gracefully into the terminal block instead of re-throwing
|
|
1615
|
+
// (a re-throw would route the caller's abort into a second unbounded
|
|
1616
|
+
// fallback stream()). Dual check as with the inner catch:
|
|
1617
|
+
// effectiveSignal.aborted (a signal we tripped) OR isAbortError(error)
|
|
1618
|
+
// (an abort-shaped throw) — either means "stop", not a real failure.
|
|
1619
|
+
if (effectiveSignal.aborted || isAbortError(error)) {
|
|
1620
|
+
wasAborted = true;
|
|
1621
|
+
break;
|
|
1599
1622
|
}
|
|
1623
|
+
logger.error("[GoogleVertex] Native SDK error", error);
|
|
1624
|
+
throw this.handleProviderError(error);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
// Handle maxSteps termination / abort — the loop exited because the step
|
|
1628
|
+
// cap was reached (or the turn was aborted) while the model was still
|
|
1629
|
+
// calling tools. Surface a real answer instead of the canned placeholder
|
|
1630
|
+
// (Bug 1) and a meaningful finishReason (Bug 2).
|
|
1631
|
+
if (!finalText && (step >= maxSteps || wasAborted)) {
|
|
1632
|
+
hitStepLimit = step >= maxSteps && !wasAborted;
|
|
1633
|
+
const toolCallCount = allToolCalls.filter((tc) => tc.toolName !== "final_result").length;
|
|
1634
|
+
// The consumer receives text via `incrementalTextChunks`; any text the
|
|
1635
|
+
// model emitted across steps is already preserved there. Only produce a
|
|
1636
|
+
// terminal message when NOTHING was produced (the pure-functionCall /
|
|
1637
|
+
// abort case that otherwise surfaces the placeholder). When chunks
|
|
1638
|
+
// exist, leave `finalText` empty so `textPartsToYield` keeps replaying
|
|
1639
|
+
// the gathered chunks instead of collapsing to a single one.
|
|
1640
|
+
if (incrementalTextChunks.length > 0) {
|
|
1641
|
+
logger.warn(`[GoogleVertex] Tool call loop terminated after reaching maxSteps (${maxSteps}); ` +
|
|
1642
|
+
`returning text already gathered from prior steps.`);
|
|
1643
|
+
}
|
|
1644
|
+
else if (wasAborted) {
|
|
1645
|
+
// Aborted turn — skip synth entirely so it can never add +300s after
|
|
1646
|
+
// a blown budget. Deliver exactly one graceful cap chunk.
|
|
1647
|
+
logger.warn(`[GoogleVertex] Tool call loop aborted mid-turn; ` +
|
|
1648
|
+
`returning a graceful cap message.`);
|
|
1649
|
+
finalText = buildToolLoopCapMessage(maxSteps, toolCallCount);
|
|
1600
1650
|
}
|
|
1601
1651
|
else {
|
|
1602
|
-
|
|
1652
|
+
logger.warn(`[GoogleVertex] Tool call loop terminated after reaching maxSteps (${maxSteps}) ` +
|
|
1653
|
+
`with no text; synthesizing a final answer with tools disabled.`);
|
|
1654
|
+
// synth self-bounds its connect+drain via withTimeout(timeoutMs) and
|
|
1655
|
+
// never throws (returns empty on timeout/error), so it needs no outer
|
|
1656
|
+
// withTimeout wrapper — see synthesizeFinalAnswerWithoutTools.
|
|
1657
|
+
const synth = await this.synthesizeFinalAnswerWithoutTools(client, modelName, config, currentContents, useFinalResultTool, parseTimeout(options.timeout) ?? DEFAULT_GEMINI_STREAM_TIMEOUT_MS, effectiveSignal);
|
|
1658
|
+
if (synth.text) {
|
|
1659
|
+
synthesizedFinalAnswer = true;
|
|
1660
|
+
finalText = synth.text;
|
|
1661
|
+
incrementalTextChunks.push(synth.text);
|
|
1662
|
+
totalInputTokens += synth.inputTokens;
|
|
1663
|
+
totalOutputTokens += synth.outputTokens;
|
|
1664
|
+
if (synth.finishReason) {
|
|
1665
|
+
lastFinishReason = synth.finishReason;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
else {
|
|
1669
|
+
finalText = buildToolLoopCapMessage(maxSteps, toolCallCount);
|
|
1670
|
+
}
|
|
1603
1671
|
}
|
|
1604
1672
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1673
|
+
}
|
|
1674
|
+
finally {
|
|
1675
|
+
clearTimeout(defensiveTimer);
|
|
1676
|
+
options.abortSignal?.removeEventListener("abort", onCallerAbort);
|
|
1609
1677
|
}
|
|
1610
1678
|
// Unified finish reason: a step-cap exhaustion that did NOT end in a clean
|
|
1611
1679
|
// synthesized answer is reported as "tool-calls" (the model still wanted
|
|
@@ -1990,173 +2058,230 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1990
2058
|
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
1991
2059
|
let totalInputTokens = 0;
|
|
1992
2060
|
let totalOutputTokens = 0;
|
|
1993
|
-
//
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2061
|
+
// Abort scaffolding (mirrors executeNativeAnthropicStream). The native
|
|
2062
|
+
// Gemini SDK cancels via config.abortSignal, so drive an internal
|
|
2063
|
+
// AbortController: the caller's signal and a defensive wall-clock timer
|
|
2064
|
+
// both trip it, and every request/tool-exec receives effectiveSignal.
|
|
2065
|
+
const streamTimeoutMs = parseTimeout(options.timeout) ?? DEFAULT_GEMINI_STREAM_TIMEOUT_MS;
|
|
2066
|
+
const internalAbort = new AbortController();
|
|
2067
|
+
const onCallerAbort = () => internalAbort.abort();
|
|
2068
|
+
options.abortSignal?.addEventListener("abort", onCallerAbort);
|
|
2069
|
+
const defensiveTimer = setTimeout(() => {
|
|
2070
|
+
logger.warn(`[GoogleVertex] Native Gemini turn exceeded ${streamTimeoutMs}ms — aborting`);
|
|
2071
|
+
internalAbort.abort();
|
|
2072
|
+
}, streamTimeoutMs);
|
|
2073
|
+
const effectiveSignal = internalAbort.signal;
|
|
2074
|
+
if (options.abortSignal?.aborted) {
|
|
2075
|
+
internalAbort.abort();
|
|
2076
|
+
}
|
|
2077
|
+
let wasAborted = false;
|
|
2078
|
+
// Step-cap flags declared in the outer scope so the terminal block (also
|
|
2079
|
+
// inside the try) and the finishReason mapping (after the finally) can
|
|
2080
|
+
// both read them.
|
|
2081
|
+
let hitStepLimit = false;
|
|
2082
|
+
let synthesizedFinalAnswer = false;
|
|
2083
|
+
try {
|
|
2084
|
+
// Agentic loop for tool calling
|
|
2085
|
+
while (step < maxSteps) {
|
|
2086
|
+
if (effectiveSignal.aborted) {
|
|
2087
|
+
wasAborted = true;
|
|
2088
|
+
break;
|
|
2089
|
+
}
|
|
2090
|
+
step++;
|
|
2091
|
+
logger.debug(`[GoogleVertex] Native SDK generate step ${step}/${maxSteps}`);
|
|
2092
|
+
try {
|
|
2093
|
+
// Use generateContentStream and collect all chunks (same as GoogleAIStudio)
|
|
2094
|
+
const stream = await client.models.generateContentStream({
|
|
2095
|
+
model: modelName,
|
|
2096
|
+
contents: currentContents,
|
|
2097
|
+
config: { ...config, abortSignal: effectiveSignal },
|
|
2098
|
+
});
|
|
2099
|
+
const stepFunctionCalls = [];
|
|
2100
|
+
// Capture raw response parts including thoughtSignature
|
|
2101
|
+
const rawResponseParts = [];
|
|
2102
|
+
// Collect all chunks from stream
|
|
2103
|
+
for await (const chunk of stream) {
|
|
2104
|
+
// Extract raw parts from candidates FIRST
|
|
2105
|
+
// This avoids using chunk.text which triggers SDK warning when
|
|
2106
|
+
// non-text parts (thoughtSignature, functionCall) are present
|
|
2107
|
+
const chunkRecord = chunk;
|
|
2108
|
+
const candidates = chunkRecord.candidates;
|
|
2109
|
+
const firstCandidate = candidates?.[0];
|
|
2110
|
+
// Capture the SDK finish reason (Bug 2: previously dropped). Last
|
|
2111
|
+
// non-empty value across chunks wins.
|
|
2112
|
+
const chunkFinishReason = firstCandidate?.finishReason;
|
|
2113
|
+
if (typeof chunkFinishReason === "string" && chunkFinishReason) {
|
|
2114
|
+
lastFinishReason = chunkFinishReason;
|
|
2036
2115
|
}
|
|
2037
|
-
|
|
2038
|
-
if (
|
|
2039
|
-
|
|
2040
|
-
|
|
2116
|
+
const chunkContent = firstCandidate?.content;
|
|
2117
|
+
if (chunkContent && Array.isArray(chunkContent.parts)) {
|
|
2118
|
+
rawResponseParts.push(...chunkContent.parts);
|
|
2119
|
+
}
|
|
2120
|
+
if (chunk.functionCalls) {
|
|
2121
|
+
stepFunctionCalls.push(...chunk.functionCalls);
|
|
2122
|
+
}
|
|
2123
|
+
// Extract usage metadata from chunk
|
|
2124
|
+
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
2125
|
+
const usageMetadata = chunkRecord.usageMetadata;
|
|
2126
|
+
if (usageMetadata) {
|
|
2127
|
+
// Take the latest promptTokenCount (usually only in final chunk)
|
|
2128
|
+
if (usageMetadata.promptTokenCount !== undefined &&
|
|
2129
|
+
usageMetadata.promptTokenCount > 0) {
|
|
2130
|
+
totalInputTokens = usageMetadata.promptTokenCount;
|
|
2131
|
+
}
|
|
2132
|
+
// Take the latest candidatesTokenCount (accumulates through chunks)
|
|
2133
|
+
if (usageMetadata.candidatesTokenCount !== undefined &&
|
|
2134
|
+
usageMetadata.candidatesTokenCount > 0) {
|
|
2135
|
+
totalOutputTokens = usageMetadata.candidatesTokenCount;
|
|
2136
|
+
}
|
|
2041
2137
|
}
|
|
2042
2138
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
finalText = stepText;
|
|
2053
|
-
break;
|
|
2054
|
-
}
|
|
2055
|
-
// Check for final_result tool call - this is our structured output pattern
|
|
2056
|
-
if (useFinalResultTool) {
|
|
2057
|
-
const finalResultCall = stepFunctionCalls.find((call) => call.name === "final_result");
|
|
2058
|
-
if (finalResultCall) {
|
|
2059
|
-
// Extract the structured output from final_result arguments
|
|
2060
|
-
finalResultStructuredOutput = finalResultCall.args;
|
|
2061
|
-
logger.debug("[GoogleVertex] Received final_result tool call with structured output (generate)", {
|
|
2062
|
-
outputKeys: Object.keys(finalResultStructuredOutput),
|
|
2063
|
-
});
|
|
2064
|
-
// Return the structured output as JSON text
|
|
2065
|
-
finalText = JSON.stringify(finalResultStructuredOutput);
|
|
2139
|
+
// Extract text from raw parts after stream completes
|
|
2140
|
+
// This avoids SDK warning about non-text parts (thoughtSignature, functionCall)
|
|
2141
|
+
const stepText = rawResponseParts
|
|
2142
|
+
.filter((part) => typeof part.text === "string")
|
|
2143
|
+
.map((part) => part.text)
|
|
2144
|
+
.join("");
|
|
2145
|
+
// If no function calls, we're done
|
|
2146
|
+
if (stepFunctionCalls.length === 0) {
|
|
2147
|
+
finalText = stepText;
|
|
2066
2148
|
break;
|
|
2067
2149
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
? rawResponseParts
|
|
2082
|
-
: stepFunctionCalls.map((fc) => ({
|
|
2083
|
-
functionCall: fc,
|
|
2084
|
-
})),
|
|
2085
|
-
});
|
|
2086
|
-
// Execute each function and collect responses
|
|
2087
|
-
const functionResponses = [];
|
|
2088
|
-
const toolCallsBefore = allToolCalls.length;
|
|
2089
|
-
const toolExecsBefore = toolExecutions.length;
|
|
2090
|
-
// Note: tool:start / tool:end events are emitted by ToolsManager's
|
|
2091
|
-
// wrapped `execute` (see ToolsManager.ts:355) — no inline emit needed.
|
|
2092
|
-
for (const call of stepFunctionCalls) {
|
|
2093
|
-
allToolCalls.push({ toolName: call.name, args: call.args });
|
|
2094
|
-
// Check if this tool has already exceeded retry limit
|
|
2095
|
-
const failedInfo = failedTools.get(call.name);
|
|
2096
|
-
if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
|
|
2097
|
-
logger.warn(`[GoogleVertex] Tool "${call.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
|
|
2098
|
-
const errorOutput = {
|
|
2099
|
-
error: `TOOL_PERMANENTLY_FAILED: The tool "${call.name}" has failed ${failedInfo.count} times and will not be retried. Last error: ${failedInfo.lastError}. Please proceed without using this tool or inform the user that this functionality is unavailable.`,
|
|
2100
|
-
status: "permanently_failed",
|
|
2101
|
-
do_not_retry: true,
|
|
2102
|
-
};
|
|
2103
|
-
toolExecutions.push({
|
|
2104
|
-
name: call.name,
|
|
2105
|
-
input: call.args,
|
|
2106
|
-
output: errorOutput,
|
|
2107
|
-
});
|
|
2108
|
-
functionResponses.push({
|
|
2109
|
-
functionResponse: {
|
|
2110
|
-
name: call.name,
|
|
2111
|
-
response: errorOutput,
|
|
2112
|
-
},
|
|
2113
|
-
});
|
|
2114
|
-
continue;
|
|
2150
|
+
// Check for final_result tool call - this is our structured output pattern
|
|
2151
|
+
if (useFinalResultTool) {
|
|
2152
|
+
const finalResultCall = stepFunctionCalls.find((call) => call.name === "final_result");
|
|
2153
|
+
if (finalResultCall) {
|
|
2154
|
+
// Extract the structured output from final_result arguments
|
|
2155
|
+
finalResultStructuredOutput = finalResultCall.args;
|
|
2156
|
+
logger.debug("[GoogleVertex] Received final_result tool call with structured output (generate)", {
|
|
2157
|
+
outputKeys: Object.keys(finalResultStructuredOutput),
|
|
2158
|
+
});
|
|
2159
|
+
// Return the structured output as JSON text
|
|
2160
|
+
finalText = JSON.stringify(finalResultStructuredOutput);
|
|
2161
|
+
break;
|
|
2162
|
+
}
|
|
2115
2163
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2164
|
+
// Accumulate non-empty step text across steps so the
|
|
2165
|
+
// maxSteps-exhaustion exit can surface the prose the model produced
|
|
2166
|
+
// instead of a canned placeholder (Bug 1). Mirrors the Vertex-Claude
|
|
2167
|
+
// loop's text accumulation.
|
|
2168
|
+
accumulatedText = appendStepText(accumulatedText, stepText);
|
|
2169
|
+
// Execute function calls
|
|
2170
|
+
logger.debug(`[GoogleVertex] Generate executing ${stepFunctionCalls.length} function calls`);
|
|
2171
|
+
// Add model response with ALL parts (including thoughtSignature) to history
|
|
2172
|
+
// This preserves the thought_signature which is required for Gemini 3 multi-turn tool calling
|
|
2173
|
+
currentContents.push({
|
|
2174
|
+
role: "model",
|
|
2175
|
+
parts: rawResponseParts.length > 0
|
|
2176
|
+
? rawResponseParts
|
|
2177
|
+
: stepFunctionCalls.map((fc) => ({
|
|
2178
|
+
functionCall: fc,
|
|
2179
|
+
})),
|
|
2180
|
+
});
|
|
2181
|
+
// Execute each function and collect responses
|
|
2182
|
+
const functionResponses = [];
|
|
2183
|
+
const toolCallsBefore = allToolCalls.length;
|
|
2184
|
+
const toolExecsBefore = toolExecutions.length;
|
|
2185
|
+
// Note: tool:start / tool:end events are emitted by ToolsManager's
|
|
2186
|
+
// wrapped `execute` (see ToolsManager.ts:355) — no inline emit needed.
|
|
2187
|
+
for (const call of stepFunctionCalls) {
|
|
2188
|
+
allToolCalls.push({ toolName: call.name, args: call.args });
|
|
2189
|
+
// Check if this tool has already exceeded retry limit
|
|
2190
|
+
const failedInfo = failedTools.get(call.name);
|
|
2191
|
+
if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
|
|
2192
|
+
logger.warn(`[GoogleVertex] Tool "${call.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
|
|
2193
|
+
const errorOutput = {
|
|
2194
|
+
error: `TOOL_PERMANENTLY_FAILED: The tool "${call.name}" has failed ${failedInfo.count} times and will not be retried. Last error: ${failedInfo.lastError}. Please proceed without using this tool or inform the user that this functionality is unavailable.`,
|
|
2195
|
+
status: "permanently_failed",
|
|
2196
|
+
do_not_retry: true,
|
|
2124
2197
|
};
|
|
2125
|
-
const execResult = await execute(call.args, toolOptions);
|
|
2126
|
-
// Track execution
|
|
2127
2198
|
toolExecutions.push({
|
|
2128
2199
|
name: call.name,
|
|
2129
2200
|
input: call.args,
|
|
2130
|
-
output:
|
|
2201
|
+
output: errorOutput,
|
|
2131
2202
|
});
|
|
2132
2203
|
functionResponses.push({
|
|
2133
2204
|
functionResponse: {
|
|
2134
2205
|
name: call.name,
|
|
2135
|
-
response:
|
|
2206
|
+
response: errorOutput,
|
|
2136
2207
|
},
|
|
2137
2208
|
});
|
|
2209
|
+
continue;
|
|
2138
2210
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2211
|
+
const execute = executeMap.get(call.name);
|
|
2212
|
+
if (execute) {
|
|
2213
|
+
try {
|
|
2214
|
+
// AI SDK Tool execute requires (args, options) - provide minimal options
|
|
2215
|
+
const toolOptions = {
|
|
2216
|
+
toolCallId: `${call.name}-${Date.now()}`,
|
|
2217
|
+
messages: [],
|
|
2218
|
+
abortSignal: effectiveSignal,
|
|
2219
|
+
};
|
|
2220
|
+
const execResult = await execute(call.args, toolOptions);
|
|
2221
|
+
// Track execution
|
|
2222
|
+
toolExecutions.push({
|
|
2223
|
+
name: call.name,
|
|
2224
|
+
input: call.args,
|
|
2225
|
+
output: execResult,
|
|
2226
|
+
});
|
|
2227
|
+
functionResponses.push({
|
|
2228
|
+
functionResponse: {
|
|
2229
|
+
name: call.name,
|
|
2230
|
+
response: { result: execResult },
|
|
2231
|
+
},
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
catch (error) {
|
|
2235
|
+
// An abort during tool execution ends the turn gracefully — it
|
|
2236
|
+
// must NOT be recorded as a spurious tool failure. Both checks
|
|
2237
|
+
// matter: effectiveSignal.aborted catches an abort WE triggered
|
|
2238
|
+
// (even if the throw isn't abort-shaped); isAbortError(error)
|
|
2239
|
+
// catches an abort-shaped throw (e.g. a tool raising AbortError)
|
|
2240
|
+
// even when the signal itself never fired.
|
|
2241
|
+
if (effectiveSignal.aborted || isAbortError(error)) {
|
|
2242
|
+
wasAborted = true;
|
|
2243
|
+
break;
|
|
2244
|
+
}
|
|
2245
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2246
|
+
// Track this failure
|
|
2247
|
+
const currentFailInfo = failedTools.get(call.name) || {
|
|
2248
|
+
count: 0,
|
|
2249
|
+
lastError: "",
|
|
2250
|
+
};
|
|
2251
|
+
currentFailInfo.count++;
|
|
2252
|
+
currentFailInfo.lastError = errorMessage;
|
|
2253
|
+
failedTools.set(call.name, currentFailInfo);
|
|
2254
|
+
logger.warn(`[GoogleVertex] Tool "${call.name}" failed (attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}): ${errorMessage}`);
|
|
2255
|
+
// Determine if this is a permanent failure
|
|
2256
|
+
const isPermanentFailure = currentFailInfo.count >= DEFAULT_TOOL_MAX_RETRIES;
|
|
2257
|
+
const errorOutput = {
|
|
2258
|
+
error: isPermanentFailure
|
|
2259
|
+
? `TOOL_PERMANENTLY_FAILED: The tool "${call.name}" has failed ${currentFailInfo.count} times with error: ${errorMessage}. This tool will not be retried. Please proceed without using this tool or inform the user that this functionality is unavailable.`
|
|
2260
|
+
: `TOOL_EXECUTION_ERROR: ${errorMessage}. Retry attempt ${currentFailInfo.count}/${DEFAULT_TOOL_MAX_RETRIES}.`,
|
|
2261
|
+
status: isPermanentFailure ? "permanently_failed" : "failed",
|
|
2262
|
+
do_not_retry: isPermanentFailure,
|
|
2263
|
+
retry_count: currentFailInfo.count,
|
|
2264
|
+
max_retries: DEFAULT_TOOL_MAX_RETRIES,
|
|
2265
|
+
};
|
|
2266
|
+
toolExecutions.push({
|
|
2267
|
+
name: call.name,
|
|
2268
|
+
input: call.args,
|
|
2269
|
+
output: errorOutput,
|
|
2270
|
+
});
|
|
2271
|
+
functionResponses.push({
|
|
2272
|
+
functionResponse: {
|
|
2273
|
+
name: call.name,
|
|
2274
|
+
response: errorOutput,
|
|
2275
|
+
},
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
else {
|
|
2280
|
+
// Tool not found is a permanent error
|
|
2152
2281
|
const errorOutput = {
|
|
2153
|
-
error:
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
status: isPermanentFailure ? "permanently_failed" : "failed",
|
|
2157
|
-
do_not_retry: isPermanentFailure,
|
|
2158
|
-
retry_count: currentFailInfo.count,
|
|
2159
|
-
max_retries: DEFAULT_TOOL_MAX_RETRIES,
|
|
2282
|
+
error: `TOOL_NOT_FOUND: The tool "${call.name}" does not exist. Do not attempt to call this tool again.`,
|
|
2283
|
+
status: "permanently_failed",
|
|
2284
|
+
do_not_retry: true,
|
|
2160
2285
|
};
|
|
2161
2286
|
toolExecutions.push({
|
|
2162
2287
|
name: call.name,
|
|
@@ -2171,101 +2296,111 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2171
2296
|
});
|
|
2172
2297
|
}
|
|
2173
2298
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
do_not_retry: true,
|
|
2180
|
-
};
|
|
2181
|
-
toolExecutions.push({
|
|
2182
|
-
name: call.name,
|
|
2183
|
-
input: call.args,
|
|
2184
|
-
output: errorOutput,
|
|
2185
|
-
});
|
|
2186
|
-
functionResponses.push({
|
|
2187
|
-
functionResponse: {
|
|
2188
|
-
name: call.name,
|
|
2189
|
-
response: errorOutput,
|
|
2190
|
-
},
|
|
2191
|
-
});
|
|
2299
|
+
// An abort inside the tool-exec loop only breaks that inner for-loop.
|
|
2300
|
+
// Break the while too so no further model call is issued and control
|
|
2301
|
+
// reaches the terminal step-cap handling below.
|
|
2302
|
+
if (wasAborted) {
|
|
2303
|
+
break;
|
|
2192
2304
|
}
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
:
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2305
|
+
// Persist this step's tool calls/results into conversation memory.
|
|
2306
|
+
// Without this, tool_call / tool_result rows never reach Redis and
|
|
2307
|
+
// the chat-history UI loses every tool invocation. The first call
|
|
2308
|
+
// of the step carries the step's `thoughtSignature` so Gemini 3 can
|
|
2309
|
+
// match thinking patterns on replay.
|
|
2310
|
+
const stepToolCalls = allToolCalls.slice(toolCallsBefore);
|
|
2311
|
+
const stepToolExecs = toolExecutions.slice(toolExecsBefore);
|
|
2312
|
+
if (stepToolCalls.length > 0 || stepToolExecs.length > 0) {
|
|
2313
|
+
const stepThoughtSig = extractThoughtSignature(rawResponseParts);
|
|
2314
|
+
withTimeout(this.handleToolExecutionStorage(stepToolCalls.map((tc, i) => ({
|
|
2315
|
+
toolName: tc.toolName,
|
|
2316
|
+
args: tc.args,
|
|
2317
|
+
...(i === 0 && stepThoughtSig
|
|
2318
|
+
? { thoughtSignature: stepThoughtSig }
|
|
2319
|
+
: {}),
|
|
2320
|
+
stepIndex: step,
|
|
2321
|
+
})), stepToolExecs.map((te) => ({
|
|
2322
|
+
toolName: te.name,
|
|
2323
|
+
output: te.output,
|
|
2324
|
+
stepIndex: step,
|
|
2325
|
+
})), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
|
|
2326
|
+
logger.warn("[GoogleVertex] Failed to store native Gemini generate tool executions", {
|
|
2327
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2328
|
+
});
|
|
2217
2329
|
});
|
|
2330
|
+
}
|
|
2331
|
+
// The @google/genai SDK only accepts "user" and "model" as valid
|
|
2332
|
+
// roles in contents — function/tool responses must use role: "user"
|
|
2333
|
+
// (matching the SDK's automaticFunctionCalling implementation and
|
|
2334
|
+
// the Google AI Studio path). See note in executeNativeGemini3Stream.
|
|
2335
|
+
currentContents.push({
|
|
2336
|
+
role: "user",
|
|
2337
|
+
parts: functionResponses,
|
|
2218
2338
|
});
|
|
2219
2339
|
}
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
logger.error("[GoogleVertex] Native SDK generate error", error);
|
|
2231
|
-
throw this.handleProviderError(error);
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
// Handle maxSteps termination — the loop exited because the step cap was
|
|
2235
|
-
// reached while the model was still calling tools. Surface a real answer
|
|
2236
|
-
// instead of the canned placeholder (Bug 1) and a meaningful finishReason
|
|
2237
|
-
// (Bug 2).
|
|
2238
|
-
let hitStepLimit = false;
|
|
2239
|
-
let synthesizedFinalAnswer = false;
|
|
2240
|
-
if (step >= maxSteps && !finalText) {
|
|
2241
|
-
hitStepLimit = true;
|
|
2242
|
-
if (accumulatedText) {
|
|
2243
|
-
// Prefer the prose the model already produced across steps.
|
|
2244
|
-
logger.warn(`[GoogleVertex] Generate tool call loop terminated after reaching maxSteps (${maxSteps}); ` +
|
|
2245
|
-
`returning text already gathered from prior steps.`);
|
|
2246
|
-
finalText = accumulatedText;
|
|
2247
|
-
}
|
|
2248
|
-
else {
|
|
2249
|
-
// Pure functionCall turns leave no text — make one tools-disabled call
|
|
2250
|
-
// so the model answers from the gathered tool results instead of the
|
|
2251
|
-
// canned placeholder.
|
|
2252
|
-
logger.warn(`[GoogleVertex] Generate tool call loop terminated after reaching maxSteps (${maxSteps}) ` +
|
|
2253
|
-
`with no text; synthesizing a final answer with tools disabled.`);
|
|
2254
|
-
const synth = await this.synthesizeFinalAnswerWithoutTools(client, modelName, config, currentContents, useFinalResultTool, parseTimeout(options.timeout) ?? 300_000);
|
|
2255
|
-
if (synth.text) {
|
|
2256
|
-
synthesizedFinalAnswer = true;
|
|
2257
|
-
finalText = synth.text;
|
|
2258
|
-
totalInputTokens += synth.inputTokens;
|
|
2259
|
-
totalOutputTokens += synth.outputTokens;
|
|
2260
|
-
if (synth.finishReason) {
|
|
2261
|
-
lastFinishReason = synth.finishReason;
|
|
2340
|
+
catch (error) {
|
|
2341
|
+
// A mid-drain abort surfaces as an AbortError from the `for await`.
|
|
2342
|
+
// Break gracefully into the terminal block instead of re-throwing
|
|
2343
|
+
// (a re-throw would route the caller's abort into a second unbounded
|
|
2344
|
+
// fallback generate()). Dual check as with the inner catch:
|
|
2345
|
+
// effectiveSignal.aborted (a signal we tripped) OR isAbortError(error)
|
|
2346
|
+
// (an abort-shaped throw) — either means "stop", not a real failure.
|
|
2347
|
+
if (effectiveSignal.aborted || isAbortError(error)) {
|
|
2348
|
+
wasAborted = true;
|
|
2349
|
+
break;
|
|
2262
2350
|
}
|
|
2351
|
+
logger.error("[GoogleVertex] Native SDK generate error", error);
|
|
2352
|
+
throw this.handleProviderError(error);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
// Handle maxSteps termination / abort — the loop exited because the step
|
|
2356
|
+
// cap was reached (or the turn was aborted) while the model was still
|
|
2357
|
+
// calling tools. Surface a real answer instead of the canned placeholder
|
|
2358
|
+
// (Bug 1) and a meaningful finishReason (Bug 2).
|
|
2359
|
+
if (!finalText && (step >= maxSteps || wasAborted)) {
|
|
2360
|
+
hitStepLimit = step >= maxSteps && !wasAborted;
|
|
2361
|
+
const toolCallCount = allToolCalls.filter((tc) => tc.toolName !== "final_result").length;
|
|
2362
|
+
if (accumulatedText) {
|
|
2363
|
+
// Prefer the prose the model already produced across steps.
|
|
2364
|
+
logger.warn(`[GoogleVertex] Generate tool call loop terminated after reaching maxSteps (${maxSteps}); ` +
|
|
2365
|
+
`returning text already gathered from prior steps.`);
|
|
2366
|
+
finalText = accumulatedText;
|
|
2367
|
+
}
|
|
2368
|
+
else if (wasAborted) {
|
|
2369
|
+
// Aborted turn — skip synth entirely so it can never add +300s after
|
|
2370
|
+
// a blown budget. Deliver a graceful cap message as ordinary prose.
|
|
2371
|
+
logger.warn(`[GoogleVertex] Generate tool call loop aborted mid-turn; ` +
|
|
2372
|
+
`returning a graceful cap message.`);
|
|
2373
|
+
finalText = buildToolLoopCapMessage(maxSteps, toolCallCount);
|
|
2263
2374
|
}
|
|
2264
2375
|
else {
|
|
2265
|
-
|
|
2376
|
+
// Pure functionCall turns leave no text — make one tools-disabled call
|
|
2377
|
+
// so the model answers from the gathered tool results instead of the
|
|
2378
|
+
// canned placeholder.
|
|
2379
|
+
logger.warn(`[GoogleVertex] Generate tool call loop terminated after reaching maxSteps (${maxSteps}) ` +
|
|
2380
|
+
`with no text; synthesizing a final answer with tools disabled.`);
|
|
2381
|
+
// synth self-bounds its connect+drain via withTimeout(timeoutMs) and
|
|
2382
|
+
// never throws (returns empty on timeout/error), so it needs no outer
|
|
2383
|
+
// withTimeout wrapper — see synthesizeFinalAnswerWithoutTools.
|
|
2384
|
+
const synth = await this.synthesizeFinalAnswerWithoutTools(client, modelName, config, currentContents, useFinalResultTool, parseTimeout(options.timeout) ?? DEFAULT_GEMINI_STREAM_TIMEOUT_MS, effectiveSignal);
|
|
2385
|
+
if (synth.text) {
|
|
2386
|
+
synthesizedFinalAnswer = true;
|
|
2387
|
+
finalText = synth.text;
|
|
2388
|
+
totalInputTokens += synth.inputTokens;
|
|
2389
|
+
totalOutputTokens += synth.outputTokens;
|
|
2390
|
+
if (synth.finishReason) {
|
|
2391
|
+
lastFinishReason = synth.finishReason;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
else {
|
|
2395
|
+
finalText = buildToolLoopCapMessage(maxSteps, toolCallCount);
|
|
2396
|
+
}
|
|
2266
2397
|
}
|
|
2267
2398
|
}
|
|
2268
2399
|
}
|
|
2400
|
+
finally {
|
|
2401
|
+
clearTimeout(defensiveTimer);
|
|
2402
|
+
options.abortSignal?.removeEventListener("abort", onCallerAbort);
|
|
2403
|
+
}
|
|
2269
2404
|
// Unified finish reason: a step-cap exhaustion that did NOT end in a clean
|
|
2270
2405
|
// synthesized answer is reported as "tool-calls" (the model still wanted
|
|
2271
2406
|
// tools) — NOT "length", which neurolink.ts treats as token truncation
|
|
@@ -2317,14 +2452,25 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2317
2452
|
* (`final_result`) pattern was active, a trailing instruction countermands the
|
|
2318
2453
|
* earlier "you MUST call final_result" directive so the model answers in plain
|
|
2319
2454
|
* text. Never throws — returns empty text so the caller falls back to the
|
|
2320
|
-
*
|
|
2455
|
+
* graceful cap message (buildToolLoopCapMessage), guaranteeing no new failure
|
|
2456
|
+
* path.
|
|
2321
2457
|
*/
|
|
2322
|
-
|
|
2458
|
+
// eslint-disable-next-line max-params -- trailing optional abortSignal keeps the positional call sites stable
|
|
2459
|
+
async synthesizeFinalAnswerWithoutTools(client, modelName, config, contents, useFinalResultTool, timeoutMs, abortSignal) {
|
|
2460
|
+
// Already aborted — never issue the synth request (would add +300s after a
|
|
2461
|
+
// blown budget). Return empty so the caller maps to the graceful message.
|
|
2462
|
+
if (abortSignal?.aborted) {
|
|
2463
|
+
return { text: "", inputTokens: 0, outputTokens: 0 };
|
|
2464
|
+
}
|
|
2323
2465
|
try {
|
|
2324
2466
|
// Shallow clone so the loop's config is never mutated; dropping the
|
|
2325
2467
|
// top-level `tools` key is sufficient (nested thinkingConfig /
|
|
2326
|
-
// systemInstruction are intentionally preserved).
|
|
2327
|
-
|
|
2468
|
+
// systemInstruction are intentionally preserved). Fold in the abort
|
|
2469
|
+
// signal so the synth request is cancellable too.
|
|
2470
|
+
const synthConfig = {
|
|
2471
|
+
...config,
|
|
2472
|
+
...(abortSignal ? { abortSignal } : {}),
|
|
2473
|
+
};
|
|
2328
2474
|
delete synthConfig.tools;
|
|
2329
2475
|
if (useFinalResultTool) {
|
|
2330
2476
|
const baseSystemInstruction = typeof synthConfig.systemInstruction === "string"
|