@juspay/neurolink 9.80.0 → 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 +12 -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 +5 -2
- package/dist/lib/providers/googleVertex.js +786 -518
- package/dist/providers/googleNativeGemini3.d.ts +20 -2
- package/dist/providers/googleNativeGemini3.js +37 -4
- package/dist/providers/googleVertex.d.ts +5 -2
- package/dist/providers/googleVertex.js +786 -518
- 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";
|
|
@@ -44,6 +44,144 @@ const hasAnthropicSupport = () => {
|
|
|
44
44
|
// Actual availability is checked at runtime when creating the client
|
|
45
45
|
return true;
|
|
46
46
|
};
|
|
47
|
+
// Parse stored rows into ordered segments, grouping tool_call/tool_result by (turn, step).
|
|
48
|
+
function collectHistorySegments(conversationMessages) {
|
|
49
|
+
const segments = [];
|
|
50
|
+
const stepMap = new Map();
|
|
51
|
+
let turnCounter = 0;
|
|
52
|
+
const getStep = (stepIndex) => {
|
|
53
|
+
const key = `${turnCounter}:${stepIndex ?? "x"}`;
|
|
54
|
+
let step = stepMap.get(key);
|
|
55
|
+
if (!step) {
|
|
56
|
+
step = { type: "tool_step", callParts: [], resultParts: [] };
|
|
57
|
+
stepMap.set(key, step);
|
|
58
|
+
segments.push(step);
|
|
59
|
+
}
|
|
60
|
+
return step;
|
|
61
|
+
};
|
|
62
|
+
for (const msg of conversationMessages) {
|
|
63
|
+
if (msg.role === "tool_call") {
|
|
64
|
+
getStep(msg.metadata?.stepIndex).callParts.push({
|
|
65
|
+
name: msg.tool || "unknown",
|
|
66
|
+
input: msg.args || {},
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (msg.role === "tool_result") {
|
|
71
|
+
getStep(msg.metadata?.stepIndex).resultParts.push(msg.content && msg.content.length > 0 ? msg.content : "(no output)");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const role = msg.role === "assistant"
|
|
75
|
+
? "assistant"
|
|
76
|
+
: msg.role === "user"
|
|
77
|
+
? "user"
|
|
78
|
+
: null;
|
|
79
|
+
if (!role || !msg.content || msg.content.trim().length === 0) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// New turn → fresh step namespace for any following tool rows.
|
|
83
|
+
turnCounter++;
|
|
84
|
+
segments.push({ type: "regular", role, parts: [msg.content] });
|
|
85
|
+
}
|
|
86
|
+
return segments;
|
|
87
|
+
}
|
|
88
|
+
// Append blocks to the trailing turn if it shares the role, else start a new turn.
|
|
89
|
+
function pushTurn(messages, role, blocks) {
|
|
90
|
+
const last = messages[messages.length - 1];
|
|
91
|
+
if (last && last.role === role && Array.isArray(last.content)) {
|
|
92
|
+
last.content.push(...blocks);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
messages.push({ role, content: [...blocks] });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Pair a tool step's calls/results by position; unbalanced extras are dropped
|
|
99
|
+
// because Anthropic 400s on an orphaned tool_use/tool_result reference.
|
|
100
|
+
function pairToolStep(step, ordinal) {
|
|
101
|
+
const calls = step.callParts;
|
|
102
|
+
const resultStrings = step.resultParts;
|
|
103
|
+
const paired = Math.min(calls.length, resultStrings.length);
|
|
104
|
+
if (paired !== calls.length || paired !== resultStrings.length) {
|
|
105
|
+
logger.debug("[GoogleVertex] Unbalanced tool step in Claude history replay", {
|
|
106
|
+
calls: calls.length,
|
|
107
|
+
results: resultStrings.length,
|
|
108
|
+
paired,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const uses = [];
|
|
112
|
+
const results = [];
|
|
113
|
+
for (let i = 0; i < paired; i++) {
|
|
114
|
+
const id = `hist_${ordinal}_${i}`;
|
|
115
|
+
uses.push({
|
|
116
|
+
type: "tool_use",
|
|
117
|
+
id,
|
|
118
|
+
name: calls[i].name,
|
|
119
|
+
input: calls[i].input,
|
|
120
|
+
});
|
|
121
|
+
results.push({
|
|
122
|
+
type: "tool_result",
|
|
123
|
+
tool_use_id: id,
|
|
124
|
+
content: resultStrings[i],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { uses, results };
|
|
128
|
+
}
|
|
129
|
+
// Drop leading turns until the first is a real user turn — Anthropic requires it
|
|
130
|
+
// (covers a leading assistant/tool step and an orphaned tool_result-only turn).
|
|
131
|
+
function trimLeadingNonUser(messages) {
|
|
132
|
+
const isToolResultOnly = (m) => Array.isArray(m.content) &&
|
|
133
|
+
m.content.length > 0 &&
|
|
134
|
+
m.content.every((block) => block.type === "tool_result");
|
|
135
|
+
while (messages.length > 0 &&
|
|
136
|
+
(messages[0].role !== "user" || isToolResultOnly(messages[0]))) {
|
|
137
|
+
messages.shift();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Rebuild Anthropic history with paired tool_use/tool_result turns from stored rows.
|
|
141
|
+
export function buildAnthropicHistoryMessages(conversationMessages) {
|
|
142
|
+
const messages = [];
|
|
143
|
+
let stepOrdinal = 0;
|
|
144
|
+
for (const seg of collectHistorySegments(conversationMessages)) {
|
|
145
|
+
if (seg.type === "regular") {
|
|
146
|
+
pushTurn(messages, seg.role === "assistant" ? "assistant" : "user", [
|
|
147
|
+
{ type: "text", text: String(seg.parts[0] ?? "") },
|
|
148
|
+
]);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const { uses, results } = pairToolStep(seg, stepOrdinal);
|
|
152
|
+
if (uses.length === 0) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
stepOrdinal++;
|
|
156
|
+
pushTurn(messages, "assistant", uses);
|
|
157
|
+
pushTurn(messages, "user", results);
|
|
158
|
+
}
|
|
159
|
+
trimLeadingNonUser(messages);
|
|
160
|
+
if (logger.shouldLog("debug")) {
|
|
161
|
+
const blocks = messages.flatMap((m) => Array.isArray(m.content) ? m.content : []);
|
|
162
|
+
logger.debug("[GoogleVertex] Replayed Claude history with tool turns", {
|
|
163
|
+
historyMessages: messages.length,
|
|
164
|
+
toolUseBlocks: blocks.filter((b) => b.type === "tool_use").length,
|
|
165
|
+
toolResultBlocks: blocks.filter((b) => b.type === "tool_result").length,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return messages;
|
|
169
|
+
}
|
|
170
|
+
// Append the live user turn, merging into a trailing user turn to avoid back-to-back user messages.
|
|
171
|
+
export function appendUserMessage(messages, content) {
|
|
172
|
+
const last = messages[messages.length - 1];
|
|
173
|
+
if (last && last.role === "user") {
|
|
174
|
+
const head = Array.isArray(last.content)
|
|
175
|
+
? last.content
|
|
176
|
+
: [{ type: "text", text: last.content }];
|
|
177
|
+
const tail = Array.isArray(content)
|
|
178
|
+
? content
|
|
179
|
+
: [{ type: "text", text: content }];
|
|
180
|
+
last.content = [...head, ...tail];
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
messages.push({ role: "user", content });
|
|
184
|
+
}
|
|
47
185
|
/**
|
|
48
186
|
* Recursively strip JSON-schema fields that Vertex Gemini's function-call AND
|
|
49
187
|
* `responseSchema` (structured output) validators reject with 400
|
|
@@ -1183,177 +1321,238 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1183
1321
|
// yielding it once breaks that signal even though the underlying
|
|
1184
1322
|
// network stream is genuinely incremental.
|
|
1185
1323
|
const incrementalTextChunks = [];
|
|
1186
|
-
//
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
+
}
|
|
1218
1384
|
}
|
|
1219
1385
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
stepFunctionCalls.push(...chunk.functionCalls);
|
|
1223
|
-
}
|
|
1224
|
-
// Extract usage metadata from chunk
|
|
1225
|
-
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
1226
|
-
const usageMetadata = chunkRecord.usageMetadata;
|
|
1227
|
-
if (usageMetadata) {
|
|
1228
|
-
// Take the latest promptTokenCount (usually only in final chunk)
|
|
1229
|
-
if (usageMetadata.promptTokenCount !== undefined &&
|
|
1230
|
-
usageMetadata.promptTokenCount > 0) {
|
|
1231
|
-
totalInputTokens = usageMetadata.promptTokenCount;
|
|
1386
|
+
if (chunk.functionCalls) {
|
|
1387
|
+
stepFunctionCalls.push(...chunk.functionCalls);
|
|
1232
1388
|
}
|
|
1233
|
-
//
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
+
}
|
|
1237
1403
|
}
|
|
1238
1404
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
finalText = stepText;
|
|
1249
|
-
break;
|
|
1250
|
-
}
|
|
1251
|
-
// Check for final_result tool call - this is our structured output pattern
|
|
1252
|
-
if (useFinalResultTool) {
|
|
1253
|
-
const finalResultCall = stepFunctionCalls.find((call) => call.name === "final_result");
|
|
1254
|
-
if (finalResultCall) {
|
|
1255
|
-
// Extract the structured output from final_result arguments
|
|
1256
|
-
finalResultStructuredOutput = finalResultCall.args;
|
|
1257
|
-
logger.debug("[GoogleVertex] Received final_result tool call with structured output (stream)", {
|
|
1258
|
-
outputKeys: Object.keys(finalResultStructuredOutput),
|
|
1259
|
-
});
|
|
1260
|
-
// Return the structured output as JSON text
|
|
1261
|
-
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;
|
|
1262
1414
|
break;
|
|
1263
1415
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
// Execute each function and collect responses
|
|
1278
|
-
const functionResponses = [];
|
|
1279
|
-
// Per-step bookkeeping for conversation-memory storage.
|
|
1280
|
-
const stepStorageCalls = [];
|
|
1281
|
-
const stepStorageResults = [];
|
|
1282
|
-
// Note: tool:start / tool:end events are emitted by ToolsManager's
|
|
1283
|
-
// wrapped `execute` (see ToolsManager.ts:355) — no inline emit needed.
|
|
1284
|
-
for (const call of stepFunctionCalls) {
|
|
1285
|
-
allToolCalls.push({ toolName: call.name, args: call.args });
|
|
1286
|
-
stepStorageCalls.push({ toolName: call.name, args: call.args });
|
|
1287
|
-
// Check if this tool has already exceeded retry limit
|
|
1288
|
-
const failedInfo = failedTools.get(call.name);
|
|
1289
|
-
if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
|
|
1290
|
-
logger.warn(`[GoogleVertex] Tool "${call.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
|
|
1291
|
-
const errorPayload = {
|
|
1292
|
-
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.`,
|
|
1293
|
-
status: "permanently_failed",
|
|
1294
|
-
do_not_retry: true,
|
|
1295
|
-
};
|
|
1296
|
-
functionResponses.push({
|
|
1297
|
-
functionResponse: {
|
|
1298
|
-
name: call.name,
|
|
1299
|
-
response: errorPayload,
|
|
1300
|
-
},
|
|
1301
|
-
});
|
|
1302
|
-
toolExecutions.push({
|
|
1303
|
-
name: call.name,
|
|
1304
|
-
input: call.args,
|
|
1305
|
-
output: errorPayload,
|
|
1306
|
-
});
|
|
1307
|
-
stepStorageResults.push({
|
|
1308
|
-
toolName: call.name,
|
|
1309
|
-
output: errorPayload,
|
|
1310
|
-
});
|
|
1311
|
-
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
|
+
}
|
|
1312
1429
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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,
|
|
1321
1460
|
};
|
|
1322
|
-
|
|
1461
|
+
functionResponses.push({
|
|
1462
|
+
functionResponse: {
|
|
1463
|
+
name: call.name,
|
|
1464
|
+
response: errorPayload,
|
|
1465
|
+
},
|
|
1466
|
+
});
|
|
1323
1467
|
toolExecutions.push({
|
|
1324
1468
|
name: call.name,
|
|
1325
1469
|
input: call.args,
|
|
1326
|
-
output:
|
|
1327
|
-
});
|
|
1328
|
-
functionResponses.push({
|
|
1329
|
-
functionResponse: { name: call.name, response: { result } },
|
|
1470
|
+
output: errorPayload,
|
|
1330
1471
|
});
|
|
1331
1472
|
stepStorageResults.push({
|
|
1332
1473
|
toolName: call.name,
|
|
1333
|
-
output:
|
|
1474
|
+
output: errorPayload,
|
|
1334
1475
|
});
|
|
1476
|
+
continue;
|
|
1335
1477
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
|
1349
1552
|
const errorPayload = {
|
|
1350
|
-
error:
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
status: isPermanentFailure ? "permanently_failed" : "failed",
|
|
1354
|
-
do_not_retry: isPermanentFailure,
|
|
1355
|
-
retry_count: currentFailInfo.count,
|
|
1356
|
-
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,
|
|
1357
1556
|
};
|
|
1358
1557
|
functionResponses.push({
|
|
1359
1558
|
functionResponse: {
|
|
@@ -1372,102 +1571,109 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1372
1571
|
});
|
|
1373
1572
|
}
|
|
1374
1573
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
do_not_retry: true,
|
|
1381
|
-
};
|
|
1382
|
-
functionResponses.push({
|
|
1383
|
-
functionResponse: {
|
|
1384
|
-
name: call.name,
|
|
1385
|
-
response: errorPayload,
|
|
1386
|
-
},
|
|
1387
|
-
});
|
|
1388
|
-
toolExecutions.push({
|
|
1389
|
-
name: call.name,
|
|
1390
|
-
input: call.args,
|
|
1391
|
-
output: errorPayload,
|
|
1392
|
-
});
|
|
1393
|
-
stepStorageResults.push({
|
|
1394
|
-
toolName: call.name,
|
|
1395
|
-
output: errorPayload,
|
|
1396
|
-
});
|
|
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;
|
|
1397
1579
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
:
|
|
1413
|
-
stepIndex: step,
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
+
});
|
|
1417
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,
|
|
1418
1610
|
});
|
|
1419
1611
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
}
|
|
1431
|
-
catch (error) {
|
|
1432
|
-
logger.error("[GoogleVertex] Native SDK error", error);
|
|
1433
|
-
throw this.handleProviderError(error);
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
// Handle maxSteps termination — the loop exited because the step cap was
|
|
1437
|
-
// reached while the model was still calling tools. Surface a real answer
|
|
1438
|
-
// instead of the canned placeholder (Bug 1) and a meaningful finishReason
|
|
1439
|
-
// (Bug 2).
|
|
1440
|
-
let hitStepLimit = false;
|
|
1441
|
-
let synthesizedFinalAnswer = false;
|
|
1442
|
-
if (step >= maxSteps && !finalText) {
|
|
1443
|
-
hitStepLimit = true;
|
|
1444
|
-
// The consumer receives text via `incrementalTextChunks`; any text the
|
|
1445
|
-
// model emitted across steps is already preserved there. Only synthesize
|
|
1446
|
-
// when NOTHING was produced (the pure-functionCall case that otherwise
|
|
1447
|
-
// surfaces the placeholder) so we never waste a round-trip whose output
|
|
1448
|
-
// the stream would ignore.
|
|
1449
|
-
if (incrementalTextChunks.length === 0) {
|
|
1450
|
-
logger.warn(`[GoogleVertex] Tool call loop terminated after reaching maxSteps (${maxSteps}) ` +
|
|
1451
|
-
`with no text; synthesizing a final answer with tools disabled.`);
|
|
1452
|
-
const synth = await this.synthesizeFinalAnswerWithoutTools(client, modelName, config, currentContents, useFinalResultTool, parseTimeout(options.timeout) ?? 300_000);
|
|
1453
|
-
if (synth.text) {
|
|
1454
|
-
synthesizedFinalAnswer = true;
|
|
1455
|
-
finalText = synth.text;
|
|
1456
|
-
incrementalTextChunks.push(synth.text);
|
|
1457
|
-
totalInputTokens += synth.inputTokens;
|
|
1458
|
-
totalOutputTokens += synth.outputTokens;
|
|
1459
|
-
if (synth.finishReason) {
|
|
1460
|
-
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;
|
|
1461
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);
|
|
1462
1650
|
}
|
|
1463
1651
|
else {
|
|
1464
|
-
|
|
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
|
+
}
|
|
1465
1671
|
}
|
|
1466
1672
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1673
|
+
}
|
|
1674
|
+
finally {
|
|
1675
|
+
clearTimeout(defensiveTimer);
|
|
1676
|
+
options.abortSignal?.removeEventListener("abort", onCallerAbort);
|
|
1471
1677
|
}
|
|
1472
1678
|
// Unified finish reason: a step-cap exhaustion that did NOT end in a clean
|
|
1473
1679
|
// synthesized answer is reported as "tool-calls" (the model still wanted
|
|
@@ -1852,173 +2058,230 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1852
2058
|
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
1853
2059
|
let totalInputTokens = 0;
|
|
1854
2060
|
let totalOutputTokens = 0;
|
|
1855
|
-
//
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
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;
|
|
1898
2115
|
}
|
|
1899
|
-
|
|
1900
|
-
if (
|
|
1901
|
-
|
|
1902
|
-
|
|
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
|
+
}
|
|
1903
2137
|
}
|
|
1904
2138
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
finalText = stepText;
|
|
1915
|
-
break;
|
|
1916
|
-
}
|
|
1917
|
-
// Check for final_result tool call - this is our structured output pattern
|
|
1918
|
-
if (useFinalResultTool) {
|
|
1919
|
-
const finalResultCall = stepFunctionCalls.find((call) => call.name === "final_result");
|
|
1920
|
-
if (finalResultCall) {
|
|
1921
|
-
// Extract the structured output from final_result arguments
|
|
1922
|
-
finalResultStructuredOutput = finalResultCall.args;
|
|
1923
|
-
logger.debug("[GoogleVertex] Received final_result tool call with structured output (generate)", {
|
|
1924
|
-
outputKeys: Object.keys(finalResultStructuredOutput),
|
|
1925
|
-
});
|
|
1926
|
-
// Return the structured output as JSON text
|
|
1927
|
-
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;
|
|
1928
2148
|
break;
|
|
1929
2149
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
? rawResponseParts
|
|
1944
|
-
: stepFunctionCalls.map((fc) => ({
|
|
1945
|
-
functionCall: fc,
|
|
1946
|
-
})),
|
|
1947
|
-
});
|
|
1948
|
-
// Execute each function and collect responses
|
|
1949
|
-
const functionResponses = [];
|
|
1950
|
-
const toolCallsBefore = allToolCalls.length;
|
|
1951
|
-
const toolExecsBefore = toolExecutions.length;
|
|
1952
|
-
// Note: tool:start / tool:end events are emitted by ToolsManager's
|
|
1953
|
-
// wrapped `execute` (see ToolsManager.ts:355) — no inline emit needed.
|
|
1954
|
-
for (const call of stepFunctionCalls) {
|
|
1955
|
-
allToolCalls.push({ toolName: call.name, args: call.args });
|
|
1956
|
-
// Check if this tool has already exceeded retry limit
|
|
1957
|
-
const failedInfo = failedTools.get(call.name);
|
|
1958
|
-
if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
|
|
1959
|
-
logger.warn(`[GoogleVertex] Tool "${call.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
|
|
1960
|
-
const errorOutput = {
|
|
1961
|
-
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.`,
|
|
1962
|
-
status: "permanently_failed",
|
|
1963
|
-
do_not_retry: true,
|
|
1964
|
-
};
|
|
1965
|
-
toolExecutions.push({
|
|
1966
|
-
name: call.name,
|
|
1967
|
-
input: call.args,
|
|
1968
|
-
output: errorOutput,
|
|
1969
|
-
});
|
|
1970
|
-
functionResponses.push({
|
|
1971
|
-
functionResponse: {
|
|
1972
|
-
name: call.name,
|
|
1973
|
-
response: errorOutput,
|
|
1974
|
-
},
|
|
1975
|
-
});
|
|
1976
|
-
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
|
+
}
|
|
1977
2163
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
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,
|
|
1986
2197
|
};
|
|
1987
|
-
const execResult = await execute(call.args, toolOptions);
|
|
1988
|
-
// Track execution
|
|
1989
2198
|
toolExecutions.push({
|
|
1990
2199
|
name: call.name,
|
|
1991
2200
|
input: call.args,
|
|
1992
|
-
output:
|
|
2201
|
+
output: errorOutput,
|
|
1993
2202
|
});
|
|
1994
2203
|
functionResponses.push({
|
|
1995
2204
|
functionResponse: {
|
|
1996
2205
|
name: call.name,
|
|
1997
|
-
response:
|
|
2206
|
+
response: errorOutput,
|
|
1998
2207
|
},
|
|
1999
2208
|
});
|
|
2209
|
+
continue;
|
|
2000
2210
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
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
|
|
2014
2281
|
const errorOutput = {
|
|
2015
|
-
error:
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
status: isPermanentFailure ? "permanently_failed" : "failed",
|
|
2019
|
-
do_not_retry: isPermanentFailure,
|
|
2020
|
-
retry_count: currentFailInfo.count,
|
|
2021
|
-
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,
|
|
2022
2285
|
};
|
|
2023
2286
|
toolExecutions.push({
|
|
2024
2287
|
name: call.name,
|
|
@@ -2033,101 +2296,111 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2033
2296
|
});
|
|
2034
2297
|
}
|
|
2035
2298
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
do_not_retry: true,
|
|
2042
|
-
};
|
|
2043
|
-
toolExecutions.push({
|
|
2044
|
-
name: call.name,
|
|
2045
|
-
input: call.args,
|
|
2046
|
-
output: errorOutput,
|
|
2047
|
-
});
|
|
2048
|
-
functionResponses.push({
|
|
2049
|
-
functionResponse: {
|
|
2050
|
-
name: call.name,
|
|
2051
|
-
response: errorOutput,
|
|
2052
|
-
},
|
|
2053
|
-
});
|
|
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;
|
|
2054
2304
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
:
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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
|
+
});
|
|
2079
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,
|
|
2080
2338
|
});
|
|
2081
2339
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
logger.error("[GoogleVertex] Native SDK generate error", error);
|
|
2093
|
-
throw this.handleProviderError(error);
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
// Handle maxSteps termination — the loop exited because the step cap was
|
|
2097
|
-
// reached while the model was still calling tools. Surface a real answer
|
|
2098
|
-
// instead of the canned placeholder (Bug 1) and a meaningful finishReason
|
|
2099
|
-
// (Bug 2).
|
|
2100
|
-
let hitStepLimit = false;
|
|
2101
|
-
let synthesizedFinalAnswer = false;
|
|
2102
|
-
if (step >= maxSteps && !finalText) {
|
|
2103
|
-
hitStepLimit = true;
|
|
2104
|
-
if (accumulatedText) {
|
|
2105
|
-
// Prefer the prose the model already produced across steps.
|
|
2106
|
-
logger.warn(`[GoogleVertex] Generate tool call loop terminated after reaching maxSteps (${maxSteps}); ` +
|
|
2107
|
-
`returning text already gathered from prior steps.`);
|
|
2108
|
-
finalText = accumulatedText;
|
|
2109
|
-
}
|
|
2110
|
-
else {
|
|
2111
|
-
// Pure functionCall turns leave no text — make one tools-disabled call
|
|
2112
|
-
// so the model answers from the gathered tool results instead of the
|
|
2113
|
-
// canned placeholder.
|
|
2114
|
-
logger.warn(`[GoogleVertex] Generate tool call loop terminated after reaching maxSteps (${maxSteps}) ` +
|
|
2115
|
-
`with no text; synthesizing a final answer with tools disabled.`);
|
|
2116
|
-
const synth = await this.synthesizeFinalAnswerWithoutTools(client, modelName, config, currentContents, useFinalResultTool, parseTimeout(options.timeout) ?? 300_000);
|
|
2117
|
-
if (synth.text) {
|
|
2118
|
-
synthesizedFinalAnswer = true;
|
|
2119
|
-
finalText = synth.text;
|
|
2120
|
-
totalInputTokens += synth.inputTokens;
|
|
2121
|
-
totalOutputTokens += synth.outputTokens;
|
|
2122
|
-
if (synth.finishReason) {
|
|
2123
|
-
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;
|
|
2124
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);
|
|
2125
2374
|
}
|
|
2126
2375
|
else {
|
|
2127
|
-
|
|
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
|
+
}
|
|
2128
2397
|
}
|
|
2129
2398
|
}
|
|
2130
2399
|
}
|
|
2400
|
+
finally {
|
|
2401
|
+
clearTimeout(defensiveTimer);
|
|
2402
|
+
options.abortSignal?.removeEventListener("abort", onCallerAbort);
|
|
2403
|
+
}
|
|
2131
2404
|
// Unified finish reason: a step-cap exhaustion that did NOT end in a clean
|
|
2132
2405
|
// synthesized answer is reported as "tool-calls" (the model still wanted
|
|
2133
2406
|
// tools) — NOT "length", which neurolink.ts treats as token truncation
|
|
@@ -2179,14 +2452,25 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2179
2452
|
* (`final_result`) pattern was active, a trailing instruction countermands the
|
|
2180
2453
|
* earlier "you MUST call final_result" directive so the model answers in plain
|
|
2181
2454
|
* text. Never throws — returns empty text so the caller falls back to the
|
|
2182
|
-
*
|
|
2455
|
+
* graceful cap message (buildToolLoopCapMessage), guaranteeing no new failure
|
|
2456
|
+
* path.
|
|
2183
2457
|
*/
|
|
2184
|
-
|
|
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
|
+
}
|
|
2185
2465
|
try {
|
|
2186
2466
|
// Shallow clone so the loop's config is never mutated; dropping the
|
|
2187
2467
|
// top-level `tools` key is sufficient (nested thinkingConfig /
|
|
2188
|
-
// systemInstruction are intentionally preserved).
|
|
2189
|
-
|
|
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
|
+
};
|
|
2190
2474
|
delete synthConfig.tools;
|
|
2191
2475
|
if (useFinalResultTool) {
|
|
2192
2476
|
const baseSystemInstruction = typeof synthConfig.systemInstruction === "string"
|
|
@@ -2287,18 +2571,14 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2287
2571
|
});
|
|
2288
2572
|
// Build messages from input
|
|
2289
2573
|
const messages = [];
|
|
2290
|
-
//
|
|
2291
|
-
//
|
|
2292
|
-
// Intentionally text-only. Anthropic's API rejects messages where a
|
|
2293
|
-
// tool_use_id reference appears without its matching tool_use in the
|
|
2294
|
-
// same turn — so synthesising tool_use / tool_result blocks from
|
|
2295
|
-
// stored ChatMessages risks emitting orphaned references that fail
|
|
2296
|
-
// validation. Tool rows are still persisted to Redis (chat-history
|
|
2297
|
-
// UI renders them) but they don't re-enter the model's context on
|
|
2298
|
-
// subsequent turns.
|
|
2574
|
+
// Replay conversationMessages (with tool turns), else the legacy text-only conversationHistory.
|
|
2299
2575
|
if (options.conversationMessages &&
|
|
2300
2576
|
options.conversationMessages.length > 0) {
|
|
2301
|
-
|
|
2577
|
+
messages.push(...buildAnthropicHistoryMessages(options.conversationMessages));
|
|
2578
|
+
}
|
|
2579
|
+
else if (options.conversationHistory &&
|
|
2580
|
+
options.conversationHistory.length > 0) {
|
|
2581
|
+
for (const msg of options.conversationHistory) {
|
|
2302
2582
|
if (msg.role === "user" || msg.role === "assistant") {
|
|
2303
2583
|
messages.push({
|
|
2304
2584
|
role: msg.role,
|
|
@@ -2430,13 +2710,10 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2430
2710
|
type: "text",
|
|
2431
2711
|
text: multimodalInput.text,
|
|
2432
2712
|
});
|
|
2433
|
-
//
|
|
2434
|
-
messages.
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
? multimodalInput.text
|
|
2438
|
-
: userContentParts,
|
|
2439
|
-
});
|
|
2713
|
+
// Append the live user turn (merges into a trailing user turn if present).
|
|
2714
|
+
appendUserMessage(messages, userContentParts.length === 1 && userContentParts[0].type === "text"
|
|
2715
|
+
? multimodalInput.text
|
|
2716
|
+
: userContentParts);
|
|
2440
2717
|
// Convert tools to Anthropic format if present
|
|
2441
2718
|
let tools;
|
|
2442
2719
|
const executeMap = new Map();
|
|
@@ -3056,20 +3333,14 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
3056
3333
|
// Build messages from input
|
|
3057
3334
|
const messages = [];
|
|
3058
3335
|
const inputText = options.prompt || options.input?.text || "Please respond.";
|
|
3059
|
-
//
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
// risks emitting orphaned references that Anthropic's API rejects.
|
|
3068
|
-
const historyMessages = options.conversationMessages && options.conversationMessages.length > 0
|
|
3069
|
-
? options.conversationMessages
|
|
3070
|
-
: options.conversationHistory;
|
|
3071
|
-
if (historyMessages && historyMessages.length > 0) {
|
|
3072
|
-
for (const msg of historyMessages) {
|
|
3336
|
+
// Replay conversationMessages (with tool turns), else the legacy text-only conversationHistory.
|
|
3337
|
+
if (options.conversationMessages &&
|
|
3338
|
+
options.conversationMessages.length > 0) {
|
|
3339
|
+
messages.push(...buildAnthropicHistoryMessages(options.conversationMessages));
|
|
3340
|
+
}
|
|
3341
|
+
else if (options.conversationHistory &&
|
|
3342
|
+
options.conversationHistory.length > 0) {
|
|
3343
|
+
for (const msg of options.conversationHistory) {
|
|
3073
3344
|
if (msg.role === "user" || msg.role === "assistant") {
|
|
3074
3345
|
messages.push({
|
|
3075
3346
|
role: msg.role,
|
|
@@ -3201,13 +3472,10 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
3201
3472
|
type: "text",
|
|
3202
3473
|
text: inputText,
|
|
3203
3474
|
});
|
|
3204
|
-
//
|
|
3205
|
-
messages.
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
? inputText
|
|
3209
|
-
: userContentParts,
|
|
3210
|
-
});
|
|
3475
|
+
// Append the live user turn (merges into a trailing user turn if present).
|
|
3476
|
+
appendUserMessage(messages, userContentParts.length === 1 && userContentParts[0].type === "text"
|
|
3477
|
+
? inputText
|
|
3478
|
+
: userContentParts);
|
|
3211
3479
|
// Convert tools to Anthropic format if present
|
|
3212
3480
|
let tools;
|
|
3213
3481
|
const executeMap = new Map();
|