@juspay/neurolink 12.39.0 → 12.40.0
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 -2
- package/README.md +1 -1
- package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
- package/dist/browser/neurolink.min.js +437 -435
- package/dist/constants/enums.d.ts +237 -0
- package/dist/constants/enums.js +257 -0
- package/dist/neurolink.js +52 -21
- package/dist/providers/catalog/ai21.json +142 -0
- package/dist/providers/catalog/aionlabs.json +144 -0
- package/dist/providers/catalog/atlas-cloud.json +203 -0
- package/dist/providers/catalog/avian-io.json +227 -0
- package/dist/providers/catalog/crusoe.json +229 -0
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +60 -0
- package/dist/providers/catalog/minimax.json +200 -0
- package/dist/providers/catalog/moonshot-ai.json +135 -0
- package/dist/providers/catalog/nebius.json +165 -0
- package/dist/providers/catalog/prime-intellect.json +217 -0
- package/dist/providers/catalog/scaleway.json +177 -0
- package/dist/providers/catalog/siliconflow.json +242 -0
- package/dist/providers/catalog/stepfun.json +155 -0
- package/dist/providers/catalog/subconscious.json +93 -0
- package/dist/providers/catalog/tinfoil.json +155 -0
- package/dist/providers/catalog/umans-ai.json +174 -0
- package/dist/providers/catalog/venice-ai.json +208 -0
- package/dist/providers/catalog/vultr-inference.json +220 -0
- package/dist/providers/catalog/wafer.json +165 -0
- package/dist/providers/catalog/wandb-inference.json +249 -0
- package/dist/providers/catalog/z-ai.json +255 -0
- package/dist/providers/googleAiStudio/client.js +687 -263
- package/dist/providers/googleNativeGemini3/utils.d.ts +44 -1
- package/dist/providers/googleNativeGemini3/utils.js +300 -1
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +80 -0
- package/docs-site/static/search-index.json +227 -3
- package/package.json +1 -1
|
@@ -18,10 +18,116 @@ import { withTimeout } from "../../utils/async/index.js";
|
|
|
18
18
|
import { estimateTokens } from "../../utils/tokenEstimation.js";
|
|
19
19
|
import { transformToolExecutions } from "../../utils/transformationUtils.js";
|
|
20
20
|
import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
|
|
21
|
-
import { buildDedupedEngineTools, buildGeminiResponseSchema, buildLoopExitMessage, buildNativeConfig, buildWrapupNudgeText, computeMaxSteps, createContextGuard, createTurnClock, buildUserPartsWithMultimodal, extractThoughtSignature, handleMaxStepsTermination, mapGeminiFinishReason, prependConversationMessages, resolveTurnStopReason, } from "../googleNativeGemini3/index.js";
|
|
21
|
+
import { buildDedupedEngineTools, buildGeminiResponseSchema, buildLoopExitMessage, buildNativeConfig, buildWrapupNudgeText, computeMaxSteps, createContextGuard, createTurnClock, buildUserPartsWithMultimodal, extractThoughtSignature, geminiContentsToV3Prompt, handleMaxStepsTermination, mapGeminiFinishReason, prependConversationMessages, resolveTurnStopReason, v3PromptToGeminiContents, } from "../googleNativeGemini3/index.js";
|
|
22
22
|
import { createStreamChannel } from "../../core/streamChannel.js";
|
|
23
23
|
import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
|
|
24
24
|
import { warnGoogleSdkIgnoresProxy } from "../../proxy/proxyFetch.js";
|
|
25
|
+
// ── Model middleware stream/generate bridges ──
|
|
26
|
+
//
|
|
27
|
+
// Caller model middleware (transformParams / wrapGenerate / wrapStream) is
|
|
28
|
+
// defined in terms of a `LanguageModelV3`'s `doGenerate`/`doStream`, not this
|
|
29
|
+
// provider's hand-rolled native loop. These two functions are the boundary
|
|
30
|
+
// between the two: `chunksToV3Stream` turns the loop's push-based channel
|
|
31
|
+
// into the `ReadableStream<LanguageModelV3StreamPart>` a `doStream` must
|
|
32
|
+
// return, and `v3StreamToChunks` turns the (possibly middleware-replaced)
|
|
33
|
+
// V3 stream that comes back out of `applyMiddlewareToModel` into the plain
|
|
34
|
+
// `{content}` chunks `StreamResult.stream` has always carried. Mirrors the
|
|
35
|
+
// identically-named pair in `openaiChatCompletionsBase.ts`; not shared with
|
|
36
|
+
// it because each native provider's PR is independent (see
|
|
37
|
+
// docs/plans/2026-09-07-middleware-on-native-providers.md).
|
|
38
|
+
/** Pulls one native chunk at a time and forwards a wrapped stream's cancel. */
|
|
39
|
+
function chunksToV3Stream(source, completion, cancel) {
|
|
40
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
41
|
+
return new ReadableStream({
|
|
42
|
+
async pull(controller) {
|
|
43
|
+
try {
|
|
44
|
+
const next = await iterator.next();
|
|
45
|
+
if (next.done) {
|
|
46
|
+
controller.enqueue(await completion);
|
|
47
|
+
controller.close();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
controller.enqueue({
|
|
51
|
+
type: "text-delta",
|
|
52
|
+
delta: next.value.content,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
controller.error(error);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
async cancel() {
|
|
61
|
+
// `config.abortSignal` is the only cancellation channel @google/genai
|
|
62
|
+
// reads (see the `sendStep` comment below), and this is what wires a
|
|
63
|
+
// caller breaking out of a wrapped stream through to it.
|
|
64
|
+
cancel();
|
|
65
|
+
await iterator.return?.();
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function* v3StreamToChunks(stream, onFinish,
|
|
70
|
+
// Called from `finally` — natural close, thrown error, AND a consumer's
|
|
71
|
+
// early `.return()` (a `break`/cancel mid-iteration) — but only when a
|
|
72
|
+
// "finish" part was never read. Lets the caller run its finish-gated
|
|
73
|
+
// cleanup (resource release, settling a pending analytics promise) even
|
|
74
|
+
// when this generator is abandoned before ever reaching one, which
|
|
75
|
+
// otherwise leaves that cleanup permanently unrun.
|
|
76
|
+
onAbandoned) {
|
|
77
|
+
const reader = stream.getReader();
|
|
78
|
+
let done = false;
|
|
79
|
+
let finishSeen = false;
|
|
80
|
+
try {
|
|
81
|
+
while (true) {
|
|
82
|
+
const next = await reader.read();
|
|
83
|
+
if (next.done) {
|
|
84
|
+
done = true;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const part = next.value;
|
|
88
|
+
if (part.type === "text-delta") {
|
|
89
|
+
yield { content: part.delta };
|
|
90
|
+
}
|
|
91
|
+
else if (part.type === "finish") {
|
|
92
|
+
finishSeen = true;
|
|
93
|
+
onFinish(part);
|
|
94
|
+
}
|
|
95
|
+
else if (part.type === "error") {
|
|
96
|
+
throw part.error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
try {
|
|
102
|
+
if (!done) {
|
|
103
|
+
await reader.cancel();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
reader.releaseLock();
|
|
108
|
+
if (!finishSeen) {
|
|
109
|
+
onAbandoned?.();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Seed the V3 prompt caller model middleware sees with the turn's current
|
|
116
|
+
* system instruction. Gemini has no wire-visible system role — the
|
|
117
|
+
* instruction rides separately on `config.systemInstruction` — so
|
|
118
|
+
* `geminiContentsToV3Prompt(contents)` alone can never emit a system
|
|
119
|
+
* message, and `transformParams` never sees the caller's real system
|
|
120
|
+
* prompt to inspect, edit or remove. Prepending it here as a leading
|
|
121
|
+
* `{role: "system"}` V3 message makes it visible; `v3PromptToGeminiContents`
|
|
122
|
+
* reads it straight back out as `systemText` on the way back in.
|
|
123
|
+
*/
|
|
124
|
+
function buildMiddlewareVisiblePrompt(config, contents) {
|
|
125
|
+
const systemInstruction = config.systemInstruction;
|
|
126
|
+
const leadingSystemMessage = typeof systemInstruction === "string" && systemInstruction.length > 0
|
|
127
|
+
? [{ role: "system", content: systemInstruction }]
|
|
128
|
+
: [];
|
|
129
|
+
return [...leadingSystemMessage, ...geminiContentsToV3Prompt(contents)];
|
|
130
|
+
}
|
|
25
131
|
// Google AI Live API types now imported from ../types/providerSpecific.js
|
|
26
132
|
// Import proper types for multimodal message handling
|
|
27
133
|
// Create Google GenAI client
|
|
@@ -588,6 +694,12 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
588
694
|
async executeStream(options, analysisSchema) {
|
|
589
695
|
const modelName = options.model || this.modelName;
|
|
590
696
|
// Phase 1: if audio input present, bridge to Gemini Live (Studio) using @google/genai
|
|
697
|
+
//
|
|
698
|
+
// Deliberately excluded from caller model middleware: Gemini Live is a
|
|
699
|
+
// persistent bidirectional session (audio in, audio/text out), not a
|
|
700
|
+
// single request/response turn, and has no `LanguageModelV3CallOptions`/
|
|
701
|
+
// `doStream` shape to run `transformParams`/`wrapStream` against. See
|
|
702
|
+
// docs/plans/2026-09-07-middleware-on-native-providers.md.
|
|
591
703
|
if (options.input?.audio) {
|
|
592
704
|
return await this.executeAudioStreamViaGeminiLive(options);
|
|
593
705
|
}
|
|
@@ -776,9 +888,15 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
776
888
|
responseTime: 0,
|
|
777
889
|
totalToolExecutions: 0,
|
|
778
890
|
};
|
|
779
|
-
//
|
|
780
|
-
// so
|
|
781
|
-
|
|
891
|
+
// The agentic loop is a named function (not an immediately-invoked
|
|
892
|
+
// one) so it can be started from inside `streamBaseModel.doStream`
|
|
893
|
+
// below, after caller model middleware has had a chance to rewrite
|
|
894
|
+
// the prompt/sampling params via `transformParams`. `runLoop`'s own
|
|
895
|
+
// two parameters shadow the outer `currentContents` / `config`, so
|
|
896
|
+
// every reference to those names in the body below picks up
|
|
897
|
+
// whatever `doStream` decided to run with, without renaming
|
|
898
|
+
// anything past this point.
|
|
899
|
+
const runLoop = async (currentContents, config) => {
|
|
782
900
|
let lastStepText = "";
|
|
783
901
|
let totalInputTokens = 0;
|
|
784
902
|
let totalOutputTokens = 0;
|
|
@@ -1145,12 +1263,163 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
1145
1263
|
upstreamSignal?.removeEventListener("abort", onUpstreamAbort);
|
|
1146
1264
|
timeoutController?.cleanup();
|
|
1147
1265
|
}
|
|
1148
|
-
}
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
|
|
1266
|
+
};
|
|
1267
|
+
// Not started yet — `streamBaseModel.doStream` starts it (assigning
|
|
1268
|
+
// this) once middleware has run `transformParams` and either calls
|
|
1269
|
+
// through or short-circuits. `v3StreamToChunks`'s finish handler
|
|
1270
|
+
// below reads this to tell the two cases apart.
|
|
1271
|
+
let loopPromise;
|
|
1272
|
+
// Seeded from `config` (buildNativeConfig's own output), not raw
|
|
1273
|
+
// `options` — `config` already applied the registry sampling-param
|
|
1274
|
+
// strip for models that reject temperature/topP. Seeding from
|
|
1275
|
+
// `options` instead would hand middleware (and, if untouched by
|
|
1276
|
+
// it, the effectiveConfig merge below) the caller's raw value and
|
|
1277
|
+
// silently reintroduce a param buildNativeConfig deliberately
|
|
1278
|
+
// dropped.
|
|
1279
|
+
const v3Params = {
|
|
1280
|
+
prompt: buildMiddlewareVisiblePrompt(config, currentContents),
|
|
1281
|
+
...(typeof config.maxOutputTokens === "number"
|
|
1282
|
+
? { maxOutputTokens: config.maxOutputTokens }
|
|
1283
|
+
: {}),
|
|
1284
|
+
...(typeof config.temperature === "number"
|
|
1285
|
+
? { temperature: config.temperature }
|
|
1286
|
+
: {}),
|
|
1287
|
+
...(typeof config.topP === "number" ? { topP: config.topP } : {}),
|
|
1288
|
+
abortSignal: composedSignal,
|
|
1289
|
+
};
|
|
1290
|
+
// The `LanguageModelV3` handle caller model middleware wraps.
|
|
1291
|
+
// `doGenerate` is never called on this path — NeuroLink drives
|
|
1292
|
+
// Google AI Studio streaming through `doStream` only — so it
|
|
1293
|
+
// throws descriptively rather than faking a result, mirroring
|
|
1294
|
+
// `buildDelegatingModel().doStream` in openaiChatCompletionsBase.ts.
|
|
1295
|
+
const streamBaseModel = {
|
|
1296
|
+
specificationVersion: "v3",
|
|
1297
|
+
provider: this.providerName,
|
|
1298
|
+
modelId: modelName,
|
|
1299
|
+
supportedUrls: {},
|
|
1300
|
+
doGenerate: () => {
|
|
1301
|
+
throw new Error("GoogleAIStudio: doGenerate is not implemented on the native stream model — NeuroLink streams through executeStream/doStream for this provider.");
|
|
1302
|
+
},
|
|
1303
|
+
doStream: async (params) => {
|
|
1304
|
+
// Honour whatever `transformParams` did to the prompt / sampling
|
|
1305
|
+
// params on the way back in, falling back to the values this
|
|
1306
|
+
// turn was built with when middleware left them untouched.
|
|
1307
|
+
const { contents: transformedContents, systemText } = v3PromptToGeminiContents(params.prompt);
|
|
1308
|
+
const effectiveConfig = {
|
|
1309
|
+
...config,
|
|
1310
|
+
...(typeof params.temperature === "number"
|
|
1311
|
+
? { temperature: params.temperature }
|
|
1312
|
+
: {}),
|
|
1313
|
+
...(typeof params.maxOutputTokens === "number"
|
|
1314
|
+
? { maxOutputTokens: params.maxOutputTokens }
|
|
1315
|
+
: {}),
|
|
1316
|
+
...(typeof params.topP === "number"
|
|
1317
|
+
? { topP: params.topP }
|
|
1318
|
+
: {}),
|
|
1319
|
+
};
|
|
1320
|
+
// Unconditional, not `...(systemText ? {...} : {})`: the
|
|
1321
|
+
// leading system message `buildMiddlewareVisiblePrompt` seeds
|
|
1322
|
+
// above means an ABSENT `systemText` is itself middleware's
|
|
1323
|
+
// answer — it stripped the caller's system prompt — not
|
|
1324
|
+
// "nothing to override". A conditional spread here would fall
|
|
1325
|
+
// through to `config`'s own `systemInstruction` and silently
|
|
1326
|
+
// undo that removal.
|
|
1327
|
+
if (systemText) {
|
|
1328
|
+
effectiveConfig.systemInstruction = systemText;
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
1331
|
+
delete effectiveConfig.systemInstruction;
|
|
1332
|
+
}
|
|
1333
|
+
loopPromise = runLoop(transformedContents, effectiveConfig);
|
|
1334
|
+
// Suppress unhandled-rejection warnings — errors are forwarded
|
|
1335
|
+
// to the channel / analyticsPromise and surface when the
|
|
1336
|
+
// caller iterates the stream or reads `.analytics`.
|
|
1337
|
+
loopPromise.catch(() => undefined);
|
|
1338
|
+
const completion = analyticsPromise.then((analytics) => ({
|
|
1339
|
+
type: "finish",
|
|
1340
|
+
finishReason: {
|
|
1341
|
+
unified: mapGeminiFinishReason(metadata.rawFinishReason),
|
|
1342
|
+
},
|
|
1343
|
+
usage: {
|
|
1344
|
+
inputTokens: { total: analytics.tokenUsage.input },
|
|
1345
|
+
outputTokens: { total: analytics.tokenUsage.output },
|
|
1346
|
+
},
|
|
1347
|
+
}));
|
|
1348
|
+
void completion.catch(() => undefined);
|
|
1349
|
+
return {
|
|
1350
|
+
stream: chunksToV3Stream(channel.iterable, completion, () => internalAbort.abort()),
|
|
1351
|
+
};
|
|
1352
|
+
},
|
|
1353
|
+
};
|
|
1354
|
+
let stream;
|
|
1355
|
+
try {
|
|
1356
|
+
const wrappedStreamModel = await this.applyMiddlewareToModel(streamBaseModel, options);
|
|
1357
|
+
if (typeof wrappedStreamModel === "string") {
|
|
1358
|
+
throw new Error("GoogleAIStudio: native stream middleware resolved to a bare model id string, expected a LanguageModelV3 handle.");
|
|
1359
|
+
}
|
|
1360
|
+
({ stream } = await wrappedStreamModel.doStream(v3Params));
|
|
1361
|
+
}
|
|
1362
|
+
catch (error) {
|
|
1363
|
+
internalAbort.abort();
|
|
1364
|
+
channel.close();
|
|
1365
|
+
turnClock.dispose();
|
|
1366
|
+
upstreamSignal?.removeEventListener("abort", onUpstreamAbort);
|
|
1367
|
+
timeoutController?.cleanup();
|
|
1368
|
+
throw this.handleProviderError(error);
|
|
1369
|
+
}
|
|
1370
|
+
// Settles the short-circuit case's analytics + releases its
|
|
1371
|
+
// per-turn resources (timeout timer, abort listener, turn clock).
|
|
1372
|
+
// Called from `v3StreamToChunks` either when a synthetic "finish"
|
|
1373
|
+
// part arrives (`part` set) or, when the stream is abandoned
|
|
1374
|
+
// before one ever does — a consumer breaking out of iteration
|
|
1375
|
+
// early, or a short-circuit stream that never emits "finish" at
|
|
1376
|
+
// all — with no `part` at all. Guarded by `!loopPromise` (a real
|
|
1377
|
+
// `runLoop` turn owns its own cleanup in its own `finally`, above)
|
|
1378
|
+
// and by `shortCircuitSettled` so a "finish" that arrives
|
|
1379
|
+
// concurrently with abandonment can't run this twice.
|
|
1380
|
+
let shortCircuitSettled = false;
|
|
1381
|
+
const settleShortCircuit = (part) => {
|
|
1382
|
+
if (loopPromise || shortCircuitSettled) {
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
shortCircuitSettled = true;
|
|
1386
|
+
const responseTime = Date.now() - startTime;
|
|
1387
|
+
const stopReason = resolveTurnStopReason({
|
|
1388
|
+
timedOut: false,
|
|
1389
|
+
stalled: false,
|
|
1390
|
+
// No "finish" part ever arrived — the turn's outcome is
|
|
1391
|
+
// unknown, so this is the closest honest fit among the
|
|
1392
|
+
// existing GenerateStopReason values (never "completed").
|
|
1393
|
+
wasAborted: !part,
|
|
1394
|
+
cappedWithoutAnswer: false,
|
|
1395
|
+
...(part ? { finishReason: part.finishReason.unified } : {}),
|
|
1396
|
+
});
|
|
1397
|
+
metadata.responseTime = responseTime;
|
|
1398
|
+
metadata.stopReason = stopReason;
|
|
1399
|
+
metadata.stepsUsed = 0;
|
|
1400
|
+
analyticsResolve({
|
|
1401
|
+
provider: this.providerName,
|
|
1402
|
+
model: modelName,
|
|
1403
|
+
tokenUsage: {
|
|
1404
|
+
input: part?.usage.inputTokens.total ?? 0,
|
|
1405
|
+
output: part?.usage.outputTokens.total ?? 0,
|
|
1406
|
+
total: (part?.usage.inputTokens.total ?? 0) +
|
|
1407
|
+
(part?.usage.outputTokens.total ?? 0),
|
|
1408
|
+
},
|
|
1409
|
+
requestDuration: responseTime,
|
|
1410
|
+
timestamp: new Date().toISOString(),
|
|
1411
|
+
stepsUsed: 0,
|
|
1412
|
+
toolCallCount: 0,
|
|
1413
|
+
stopReason,
|
|
1414
|
+
elapsedMs: responseTime,
|
|
1415
|
+
});
|
|
1416
|
+
turnClock.dispose();
|
|
1417
|
+
upstreamSignal?.removeEventListener("abort", onUpstreamAbort);
|
|
1418
|
+
timeoutController?.cleanup();
|
|
1419
|
+
};
|
|
1420
|
+
const chunkSource = v3StreamToChunks(stream, (part) => settleShortCircuit(part), () => settleShortCircuit());
|
|
1152
1421
|
const result = {
|
|
1153
|
-
stream:
|
|
1422
|
+
stream: chunkSource,
|
|
1154
1423
|
provider: this.providerName,
|
|
1155
1424
|
model: modelName,
|
|
1156
1425
|
toolCalls: allToolCalls,
|
|
@@ -1313,281 +1582,436 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
1313
1582
|
const hitTimeLimit = () => turnClock.timedOut ||
|
|
1314
1583
|
timeoutController?.controller.signal.aborted === true;
|
|
1315
1584
|
const maxSteps = computeMaxSteps(options.maxSteps);
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1585
|
+
// Named function (not inlined): `generateBaseModel.doGenerate`
|
|
1586
|
+
// below starts it once caller model middleware has run
|
|
1587
|
+
// `transformParams` on the prompt / sampling params. Its two
|
|
1588
|
+
// parameters shadow the outer `currentContents` / `config`, so
|
|
1589
|
+
// every reference to those names in the body below picks up
|
|
1590
|
+
// whatever `doGenerate` decided to run with, without renaming
|
|
1591
|
+
// anything past this point.
|
|
1592
|
+
const runGenerateLoop = async (currentContents, config) => {
|
|
1593
|
+
let finalText = "";
|
|
1594
|
+
let lastStepText = "";
|
|
1595
|
+
let totalInputTokens = 0;
|
|
1596
|
+
let totalOutputTokens = 0;
|
|
1597
|
+
let totalCacheReadTokens = 0;
|
|
1598
|
+
let totalReasoningTokens = 0;
|
|
1599
|
+
const allToolCalls = [];
|
|
1600
|
+
const toolExecutions = [];
|
|
1601
|
+
let step = 0;
|
|
1602
|
+
// Model calls made — see the streaming twin for why this is not
|
|
1603
|
+
// `step`.
|
|
1604
|
+
let stepsTaken = 0;
|
|
1605
|
+
let wasAborted = false;
|
|
1606
|
+
let lastFinishReason;
|
|
1607
|
+
// Cheap reclaim trigger — see the stream twin.
|
|
1608
|
+
const contextGuard = createContextGuard(getContextWindowSize("googleAiStudio", modelName));
|
|
1609
|
+
// Agentic loop for tool calling
|
|
1610
|
+
// Same shared engine as the streaming twin. This path has no
|
|
1611
|
+
// consumer channel — generate() returns one result rather than
|
|
1612
|
+
// streaming — so the engine's stream is drained and discarded, and
|
|
1613
|
+
// the turn's text comes from the result.
|
|
1614
|
+
const baseAdapter = createGeminiLoopAdapter({
|
|
1615
|
+
providerLabel: "GoogleAIStudio",
|
|
1616
|
+
maxSteps,
|
|
1617
|
+
toolFailureBreaker: { maxRetries: DEFAULT_TOOL_MAX_RETRIES },
|
|
1618
|
+
liveTools: options.tools ?? {},
|
|
1619
|
+
toolGuards: {
|
|
1620
|
+
toolTimeoutMs: toolExecTimeoutMs,
|
|
1621
|
+
abortSignal: composedSignal,
|
|
1622
|
+
onProgress: () => turnClock.noteProgress(),
|
|
1623
|
+
},
|
|
1624
|
+
...(declarationsResult
|
|
1625
|
+
? { declarations: declarationsResult }
|
|
1626
|
+
: {}),
|
|
1627
|
+
buildRequest: (contents) => ({
|
|
1628
|
+
model: modelName,
|
|
1629
|
+
contents,
|
|
1630
|
+
config,
|
|
1631
|
+
}),
|
|
1632
|
+
// See the streaming twin: `config.abortSignal` is the only
|
|
1633
|
+
// cancellation channel the SDK reads.
|
|
1634
|
+
sendStep: async (request, signal) => {
|
|
1635
|
+
turnClock.noteProgress();
|
|
1636
|
+
const built = request;
|
|
1637
|
+
return client.models.generateContentStream({
|
|
1638
|
+
...built,
|
|
1639
|
+
config: { ...(built.config ?? {}), abortSignal: signal },
|
|
1640
|
+
});
|
|
1641
|
+
},
|
|
1642
|
+
noteUsage: (inputTokens, outputTokens) => {
|
|
1643
|
+
contextGuard.noteUsage(inputTokens, outputTokens);
|
|
1644
|
+
},
|
|
1645
|
+
planReclaim: (contents, stepIndex) => {
|
|
1646
|
+
if (stepIndex !== 0 && !contextGuard.shouldStop()) {
|
|
1647
|
+
return undefined;
|
|
1648
|
+
}
|
|
1649
|
+
const working = [...contents];
|
|
1650
|
+
if (!reclaimAiStudioContext(working, modelName, contextGuard.projectedNextPromptTokens)) {
|
|
1651
|
+
return undefined;
|
|
1652
|
+
}
|
|
1653
|
+
contextGuard.resetAfterReclaim();
|
|
1654
|
+
return { conversation: working };
|
|
1655
|
+
},
|
|
1656
|
+
});
|
|
1657
|
+
const adapter = {
|
|
1658
|
+
...baseAdapter,
|
|
1659
|
+
// Once per model call, including the final text-only step — see
|
|
1660
|
+
// the streaming twin.
|
|
1661
|
+
buildStepRequest: (contents, engineStep) => {
|
|
1662
|
+
stepsTaken = engineStep + 1;
|
|
1663
|
+
turnClock.noteProgress();
|
|
1664
|
+
return baseAdapter.buildStepRequest(contents, engineStep);
|
|
1665
|
+
},
|
|
1666
|
+
buildToolResultMessages: (contents, stepResult, toolResults, engineStep) => {
|
|
1667
|
+
// Same as the streaming twin: the engine's step, not a count of
|
|
1668
|
+
// hook invocations. See the comment there.
|
|
1669
|
+
step = engineStep + 1;
|
|
1670
|
+
for (const call of stepResult.toolCalls) {
|
|
1671
|
+
span.addEvent("gen_ai.tool_call", {
|
|
1672
|
+
"tool.name": call.name,
|
|
1673
|
+
"tool.step": step,
|
|
1674
|
+
});
|
|
1675
|
+
allToolCalls.push({ toolName: call.name, args: call.args });
|
|
1676
|
+
}
|
|
1677
|
+
lastStepText = stepResult.text || lastStepText;
|
|
1678
|
+
for (const result of toolResults) {
|
|
1679
|
+
toolExecutions.push({
|
|
1680
|
+
name: result.name,
|
|
1681
|
+
input: result.args,
|
|
1682
|
+
output: result.output,
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
if (toolResults.length > 0) {
|
|
1686
|
+
const stepThoughtSig = extractThoughtSignature(stepResult.raw.rawResponseParts);
|
|
1687
|
+
withTimeout(this.handleToolExecutionStorage(stepResult.toolCalls.map((call, i) => ({
|
|
1688
|
+
toolName: call.name,
|
|
1689
|
+
args: call.args,
|
|
1690
|
+
...(i === 0 && stepThoughtSig
|
|
1691
|
+
? { thoughtSignature: stepThoughtSig }
|
|
1692
|
+
: {}),
|
|
1693
|
+
stepIndex: step,
|
|
1694
|
+
})), toolResults.map((result) => ({
|
|
1695
|
+
toolName: result.name,
|
|
1696
|
+
output: result.output,
|
|
1697
|
+
stepIndex: step,
|
|
1698
|
+
})), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
|
|
1699
|
+
logger.warn("[GoogleAIStudio] Failed to store native tool executions", {
|
|
1700
|
+
error: error instanceof Error
|
|
1701
|
+
? error.message
|
|
1702
|
+
: String(error),
|
|
1703
|
+
});
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
const next = baseAdapter.buildToolResultMessages(contents, stepResult, toolResults, engineStep);
|
|
1707
|
+
// Wrap-up nudge — see the streaming twin.
|
|
1708
|
+
if (turnClock.shouldNudgeWrapup()) {
|
|
1709
|
+
const last = next[next.length - 1];
|
|
1710
|
+
if (last && Array.isArray(last.parts)) {
|
|
1711
|
+
last.parts.push({ text: buildWrapupNudgeText(false) });
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
try {
|
|
1715
|
+
const appended = next[next.length - 1];
|
|
1716
|
+
contextGuard.noteAppendedChars(JSON.stringify(appended?.parts ?? []).length);
|
|
1717
|
+
}
|
|
1718
|
+
catch {
|
|
1719
|
+
/* estimation is best-effort — never break the loop */
|
|
1720
|
+
}
|
|
1721
|
+
return next;
|
|
1722
|
+
},
|
|
1723
|
+
};
|
|
1724
|
+
// Same dedup routing as the streaming twin above.
|
|
1725
|
+
const engineTools = buildDedupedEngineTools(declarationsResult, options.tools, {
|
|
1343
1726
|
toolTimeoutMs: toolExecTimeoutMs,
|
|
1344
1727
|
abortSignal: composedSignal,
|
|
1345
1728
|
onProgress: () => turnClock.noteProgress(),
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
//
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
return client.models.generateContentStream({
|
|
1359
|
-
...built,
|
|
1360
|
-
config: { ...(built.config ?? {}), abortSignal: signal },
|
|
1361
|
-
});
|
|
1362
|
-
},
|
|
1363
|
-
noteUsage: (inputTokens, outputTokens) => {
|
|
1364
|
-
contextGuard.noteUsage(inputTokens, outputTokens);
|
|
1365
|
-
},
|
|
1366
|
-
planReclaim: (contents, stepIndex) => {
|
|
1367
|
-
if (stepIndex !== 0 && !contextGuard.shouldStop()) {
|
|
1368
|
-
return undefined;
|
|
1729
|
+
});
|
|
1730
|
+
const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentContents, {
|
|
1731
|
+
tools: engineTools,
|
|
1732
|
+
abortSignal: composedSignal,
|
|
1733
|
+
toolTimeoutMs: toolExecTimeoutMs,
|
|
1734
|
+
});
|
|
1735
|
+
// Drained, not consumed: nothing streams out of generate(), but an
|
|
1736
|
+
// undrained channel would stall the engine mid-turn.
|
|
1737
|
+
const drain = (async () => {
|
|
1738
|
+
for await (const chunk of engineStream) {
|
|
1739
|
+
void chunk;
|
|
1740
|
+
turnClock.noteProgress();
|
|
1369
1741
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1742
|
+
})();
|
|
1743
|
+
let engineResult;
|
|
1744
|
+
let turnFailure;
|
|
1745
|
+
try {
|
|
1746
|
+
engineResult = await resultPromise;
|
|
1747
|
+
}
|
|
1748
|
+
catch (error) {
|
|
1749
|
+
turnFailure = error;
|
|
1750
|
+
}
|
|
1751
|
+
await drainDetachedPump(drain, "GoogleAIStudio");
|
|
1752
|
+
if (turnFailure !== undefined) {
|
|
1753
|
+
// Same split as the streaming twin: a blown time budget or a
|
|
1754
|
+
// caller abort ends the turn honestly, a provider failure still
|
|
1755
|
+
// throws.
|
|
1756
|
+
if (turnClock.expired || options.abortSignal?.aborted) {
|
|
1757
|
+
wasAborted = true;
|
|
1373
1758
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
});
|
|
1378
|
-
const adapter = {
|
|
1379
|
-
...baseAdapter,
|
|
1380
|
-
// Once per model call, including the final text-only step — see
|
|
1381
|
-
// the streaming twin.
|
|
1382
|
-
buildStepRequest: (contents, engineStep) => {
|
|
1383
|
-
stepsTaken = engineStep + 1;
|
|
1384
|
-
turnClock.noteProgress();
|
|
1385
|
-
return baseAdapter.buildStepRequest(contents, engineStep);
|
|
1386
|
-
},
|
|
1387
|
-
buildToolResultMessages: (contents, stepResult, toolResults, engineStep) => {
|
|
1388
|
-
// Same as the streaming twin: the engine's step, not a count of
|
|
1389
|
-
// hook invocations. See the comment there.
|
|
1390
|
-
step = engineStep + 1;
|
|
1391
|
-
for (const call of stepResult.toolCalls) {
|
|
1392
|
-
span.addEvent("gen_ai.tool_call", {
|
|
1393
|
-
"tool.name": call.name,
|
|
1394
|
-
"tool.step": step,
|
|
1395
|
-
});
|
|
1396
|
-
allToolCalls.push({ toolName: call.name, args: call.args });
|
|
1759
|
+
else {
|
|
1760
|
+
logger.error("[GoogleAIStudio] Native SDK generate error", turnFailure);
|
|
1761
|
+
throw this.handleProviderError(turnFailure);
|
|
1397
1762
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1763
|
+
}
|
|
1764
|
+
if (engineResult) {
|
|
1765
|
+
totalInputTokens += engineResult.usage.inputTokens;
|
|
1766
|
+
totalOutputTokens += engineResult.usage.outputTokens;
|
|
1767
|
+
totalCacheReadTokens += engineResult.usage.cacheReadTokens ?? 0;
|
|
1768
|
+
totalReasoningTokens += engineResult.usage.reasoningTokens ?? 0;
|
|
1769
|
+
lastFinishReason = engineResult.rawStopReason ?? lastFinishReason;
|
|
1770
|
+
finalText = engineResult.text;
|
|
1771
|
+
if (engineResult.aborted) {
|
|
1772
|
+
wasAborted = true;
|
|
1405
1773
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1774
|
+
}
|
|
1775
|
+
if (composedSignal.aborted) {
|
|
1776
|
+
wasAborted = true;
|
|
1777
|
+
}
|
|
1778
|
+
const hitStepLimitWithoutAnswer = !wasAborted && step >= maxSteps && !finalText;
|
|
1779
|
+
if (wasAborted && !finalText) {
|
|
1780
|
+
// Never the step-cap text for a turn the budget killed: prefer
|
|
1781
|
+
// the prose the model already produced, else an honest message
|
|
1782
|
+
// naming the actual exit cause.
|
|
1783
|
+
logger.warn(`[GoogleAIStudio] Generate tool call loop ended mid-turn ` +
|
|
1784
|
+
`(${hitTimeLimit() ? "turn time limit" : turnClock.stalled ? "stall watchdog" : "caller abort"}); ` +
|
|
1785
|
+
`returning gathered text or an honest terminal message.`);
|
|
1786
|
+
finalText =
|
|
1787
|
+
lastStepText ||
|
|
1788
|
+
buildLoopExitMessage({
|
|
1789
|
+
timedOut: hitTimeLimit(),
|
|
1790
|
+
stalled: turnClock.stalled,
|
|
1791
|
+
elapsedMs: turnClock.elapsedMs(),
|
|
1792
|
+
wasAborted,
|
|
1793
|
+
...(options.stallTimeoutMs !== undefined
|
|
1794
|
+
? { stallTimeoutMs: options.stallTimeoutMs }
|
|
1795
|
+
: {}),
|
|
1796
|
+
maxSteps,
|
|
1797
|
+
toolCallCount: allToolCalls.length,
|
|
1422
1798
|
});
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1799
|
+
}
|
|
1800
|
+
else {
|
|
1801
|
+
finalText = handleMaxStepsTermination("[GoogleAIStudio]", step, maxSteps, finalText, lastStepText);
|
|
1802
|
+
}
|
|
1803
|
+
const responseTime = Date.now() - startTime;
|
|
1804
|
+
// Turn-exit discriminator — see the streaming twin.
|
|
1805
|
+
const stopReason = resolveTurnStopReason({
|
|
1806
|
+
timedOut: hitTimeLimit(),
|
|
1807
|
+
stalled: turnClock.stalled,
|
|
1808
|
+
wasAborted,
|
|
1809
|
+
cappedWithoutAnswer: hitStepLimitWithoutAnswer,
|
|
1810
|
+
finishReason: mapGeminiFinishReason(lastFinishReason),
|
|
1811
|
+
});
|
|
1812
|
+
if (stopReason !== "completed") {
|
|
1813
|
+
this.emitTurnEvent({
|
|
1814
|
+
phase: stopReason,
|
|
1815
|
+
step: stepsTaken,
|
|
1816
|
+
maxSteps,
|
|
1817
|
+
toolCallCount: allToolCalls.length,
|
|
1818
|
+
elapsedMs: turnClock.elapsedMs(),
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
// Set token usage and finish reason on the span
|
|
1822
|
+
span.setAttribute(ATTR.GEN_AI_INPUT_TOKENS, totalInputTokens);
|
|
1823
|
+
span.setAttribute(ATTR.GEN_AI_OUTPUT_TOKENS, totalOutputTokens);
|
|
1824
|
+
span.setAttribute(ATTR.GEN_AI_FINISH_REASON, step >= maxSteps ? "max_steps" : "stop");
|
|
1825
|
+
// Build EnhancedGenerateResult and route through enhanceResult so
|
|
1826
|
+
// analytics / evaluation / tracing stay attached. The native AI
|
|
1827
|
+
// Studio generate path bypasses BaseProvider.generate(), so
|
|
1828
|
+
// skipping enhanceResult would silently drop those features.
|
|
1829
|
+
// Gemini promptTokenCount is OVERLAPPING (already includes
|
|
1830
|
+
// cachedContentTokenCount). Subtract once so the cached portion is
|
|
1831
|
+
// billed at the cheaper cacheRead rate without double-counting.
|
|
1832
|
+
const adjustedInputTokens = Math.max(0, totalInputTokens - totalCacheReadTokens);
|
|
1833
|
+
const baseResult = {
|
|
1834
|
+
content: finalText,
|
|
1835
|
+
provider: this.providerName,
|
|
1836
|
+
model: modelName,
|
|
1837
|
+
// createAnalytics reads these off the result, so setting them
|
|
1838
|
+
// here is also what puts stepsUsed / stopReason / elapsedMs /
|
|
1839
|
+
// rawFinishReason on `result.analytics`.
|
|
1840
|
+
stopReason,
|
|
1841
|
+
stepsUsed: stepsTaken,
|
|
1842
|
+
...(lastFinishReason !== undefined
|
|
1843
|
+
? { rawFinishReason: lastFinishReason }
|
|
1844
|
+
: {}),
|
|
1845
|
+
usage: {
|
|
1846
|
+
input: adjustedInputTokens,
|
|
1847
|
+
// Thinking tokens are billed at the output rate but Gemini
|
|
1848
|
+
// does NOT include them in candidatesTokenCount, so they are
|
|
1849
|
+
// folded into `output` — what calculateCost bills at the
|
|
1850
|
+
// output rate — with `reasoning` as the subset.
|
|
1851
|
+
output: totalOutputTokens + totalReasoningTokens,
|
|
1852
|
+
total: adjustedInputTokens +
|
|
1853
|
+
totalCacheReadTokens +
|
|
1854
|
+
totalOutputTokens +
|
|
1855
|
+
totalReasoningTokens,
|
|
1856
|
+
...(totalCacheReadTokens > 0
|
|
1857
|
+
? { cacheReadTokens: totalCacheReadTokens }
|
|
1858
|
+
: {}),
|
|
1859
|
+
...(totalReasoningTokens > 0
|
|
1860
|
+
? { reasoning: totalReasoningTokens }
|
|
1861
|
+
: {}),
|
|
1862
|
+
},
|
|
1863
|
+
...(totalReasoningTokens > 0 && {
|
|
1864
|
+
reasoningTokens: totalReasoningTokens,
|
|
1865
|
+
}),
|
|
1866
|
+
responseTime,
|
|
1867
|
+
toolsUsed: allToolCalls.map((tc) => tc.toolName),
|
|
1868
|
+
toolExecutions: resolveToolExecutionRecords(options, toolExecutions),
|
|
1869
|
+
enhancedWithTools: allToolCalls.length > 0,
|
|
1870
|
+
};
|
|
1871
|
+
return baseResult;
|
|
1872
|
+
};
|
|
1873
|
+
// Not run yet — `generateBaseModel.doGenerate` runs it (assigning
|
|
1874
|
+
// `turnBaseResult`) once caller model middleware has had its turn
|
|
1875
|
+
// and either calls through or short-circuits. Absence of
|
|
1876
|
+
// `turnBaseResult` after the call below is how the short-circuit
|
|
1877
|
+
// case is told apart from a real turn, mirroring the streaming
|
|
1878
|
+
// twin's `!loopPromise` check.
|
|
1879
|
+
let turnBaseResult;
|
|
1880
|
+
// Seeded from `config` (buildNativeConfig's own output), not raw
|
|
1881
|
+
// `options` — see the streaming twin's identical comment: `config`
|
|
1882
|
+
// already applied the registry sampling-param strip for models
|
|
1883
|
+
// that reject temperature/topP, and seeding from raw `options`
|
|
1884
|
+
// would silently reintroduce a param buildNativeConfig dropped.
|
|
1885
|
+
const v3Params = {
|
|
1886
|
+
prompt: buildMiddlewareVisiblePrompt(config, currentContents),
|
|
1887
|
+
...(typeof config.maxOutputTokens === "number"
|
|
1888
|
+
? { maxOutputTokens: config.maxOutputTokens }
|
|
1889
|
+
: {}),
|
|
1890
|
+
...(typeof config.temperature === "number"
|
|
1891
|
+
? { temperature: config.temperature }
|
|
1892
|
+
: {}),
|
|
1893
|
+
...(typeof config.topP === "number" ? { topP: config.topP } : {}),
|
|
1894
|
+
abortSignal: composedSignal,
|
|
1895
|
+
};
|
|
1896
|
+
// The `LanguageModelV3` handle caller model middleware wraps.
|
|
1897
|
+
// `doStream` is never called on this path — NeuroLink runs Google
|
|
1898
|
+
// AI Studio's synchronous turn through `doGenerate` only — so it
|
|
1899
|
+
// throws descriptively rather than faking a result, mirroring
|
|
1900
|
+
// `buildDelegatingModel().doStream` in openaiChatCompletionsBase.ts.
|
|
1901
|
+
const generateBaseModel = {
|
|
1902
|
+
specificationVersion: "v3",
|
|
1903
|
+
provider: this.providerName,
|
|
1904
|
+
modelId: modelName,
|
|
1905
|
+
supportedUrls: {},
|
|
1906
|
+
doStream: () => {
|
|
1907
|
+
throw new Error("GoogleAIStudio: doStream is not implemented on the native generate model — NeuroLink runs this provider's turn synchronously through doGenerate.");
|
|
1908
|
+
},
|
|
1909
|
+
doGenerate: async (params) => {
|
|
1910
|
+
// Honour whatever `transformParams` did to the prompt /
|
|
1911
|
+
// sampling params on the way back in, falling back to the
|
|
1912
|
+
// values this turn was built with when middleware left them
|
|
1913
|
+
// untouched.
|
|
1914
|
+
const { contents: transformedContents, systemText } = v3PromptToGeminiContents(params.prompt);
|
|
1915
|
+
const effectiveConfig = {
|
|
1916
|
+
...config,
|
|
1917
|
+
...(typeof params.temperature === "number"
|
|
1918
|
+
? { temperature: params.temperature }
|
|
1919
|
+
: {}),
|
|
1920
|
+
...(typeof params.maxOutputTokens === "number"
|
|
1921
|
+
? { maxOutputTokens: params.maxOutputTokens }
|
|
1922
|
+
: {}),
|
|
1923
|
+
...(typeof params.topP === "number"
|
|
1924
|
+
? { topP: params.topP }
|
|
1925
|
+
: {}),
|
|
1926
|
+
};
|
|
1927
|
+
// Unconditional — see the streaming twin's identical comment:
|
|
1928
|
+
// an absent `systemText` here means middleware deliberately
|
|
1929
|
+
// removed the caller's system prompt (seeded in by
|
|
1930
|
+
// `buildMiddlewareVisiblePrompt` above), not that there is
|
|
1931
|
+
// nothing to override.
|
|
1932
|
+
if (systemText) {
|
|
1933
|
+
effectiveConfig.systemInstruction = systemText;
|
|
1436
1934
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1935
|
+
else {
|
|
1936
|
+
delete effectiveConfig.systemInstruction;
|
|
1439
1937
|
}
|
|
1440
|
-
|
|
1938
|
+
const loopResult = await runGenerateLoop(transformedContents, effectiveConfig);
|
|
1939
|
+
turnBaseResult = loopResult;
|
|
1940
|
+
return {
|
|
1941
|
+
content: loopResult.content
|
|
1942
|
+
? [{ type: "text", text: loopResult.content }]
|
|
1943
|
+
: [],
|
|
1944
|
+
finishReason: {
|
|
1945
|
+
unified: mapGeminiFinishReason(loopResult.rawFinishReason),
|
|
1946
|
+
},
|
|
1947
|
+
usage: {
|
|
1948
|
+
inputTokens: { total: loopResult.usage?.input ?? 0 },
|
|
1949
|
+
outputTokens: { total: loopResult.usage?.output ?? 0 },
|
|
1950
|
+
},
|
|
1951
|
+
};
|
|
1441
1952
|
},
|
|
1442
1953
|
};
|
|
1443
|
-
|
|
1444
|
-
const engineTools = buildDedupedEngineTools(declarationsResult, options.tools, {
|
|
1445
|
-
toolTimeoutMs: toolExecTimeoutMs,
|
|
1446
|
-
abortSignal: composedSignal,
|
|
1447
|
-
onProgress: () => turnClock.noteProgress(),
|
|
1448
|
-
});
|
|
1449
|
-
const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentContents, {
|
|
1450
|
-
tools: engineTools,
|
|
1451
|
-
abortSignal: composedSignal,
|
|
1452
|
-
toolTimeoutMs: toolExecTimeoutMs,
|
|
1453
|
-
});
|
|
1454
|
-
// Drained, not consumed: nothing streams out of generate(), but an
|
|
1455
|
-
// undrained channel would stall the engine mid-turn.
|
|
1456
|
-
const drain = (async () => {
|
|
1457
|
-
for await (const chunk of engineStream) {
|
|
1458
|
-
void chunk;
|
|
1459
|
-
turnClock.noteProgress();
|
|
1460
|
-
}
|
|
1461
|
-
})();
|
|
1462
|
-
let engineResult;
|
|
1463
|
-
let turnFailure;
|
|
1954
|
+
let v3Result;
|
|
1464
1955
|
try {
|
|
1465
|
-
|
|
1956
|
+
const wrappedGenerateModel = await this.applyMiddlewareToModel(generateBaseModel, options);
|
|
1957
|
+
if (typeof wrappedGenerateModel === "string") {
|
|
1958
|
+
throw new Error("GoogleAIStudio: native generate middleware resolved to a bare model id string, expected a LanguageModelV3 handle.");
|
|
1959
|
+
}
|
|
1960
|
+
v3Result = await wrappedGenerateModel.doGenerate(v3Params);
|
|
1466
1961
|
}
|
|
1467
1962
|
catch (error) {
|
|
1468
|
-
|
|
1963
|
+
throw this.handleProviderError(error);
|
|
1469
1964
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1965
|
+
// `doGenerate`'s post-middleware result is authoritative for
|
|
1966
|
+
// content/usage/finishReason — that is the whole point of
|
|
1967
|
+
// `wrapGenerate` being able to replace it. Turn-lifecycle
|
|
1968
|
+
// telemetry that has no V3 equivalent (stopReason, stepsUsed,
|
|
1969
|
+
// tool bookkeeping) comes from `turnBaseResult` when the real
|
|
1970
|
+
// turn ran, and from short-circuit defaults when it did not.
|
|
1971
|
+
let finalContent = "";
|
|
1972
|
+
for (const part of v3Result.content) {
|
|
1973
|
+
if (part.type === "text") {
|
|
1974
|
+
finalContent += part.text;
|
|
1477
1975
|
}
|
|
1478
|
-
else {
|
|
1479
|
-
logger.error("[GoogleAIStudio] Native SDK generate error", turnFailure);
|
|
1480
|
-
throw this.handleProviderError(turnFailure);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
if (engineResult) {
|
|
1484
|
-
totalInputTokens += engineResult.usage.inputTokens;
|
|
1485
|
-
totalOutputTokens += engineResult.usage.outputTokens;
|
|
1486
|
-
totalCacheReadTokens += engineResult.usage.cacheReadTokens ?? 0;
|
|
1487
|
-
totalReasoningTokens += engineResult.usage.reasoningTokens ?? 0;
|
|
1488
|
-
lastFinishReason = engineResult.rawStopReason ?? lastFinishReason;
|
|
1489
|
-
finalText = engineResult.text;
|
|
1490
|
-
if (engineResult.aborted) {
|
|
1491
|
-
wasAborted = true;
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
if (composedSignal.aborted) {
|
|
1495
|
-
wasAborted = true;
|
|
1496
|
-
}
|
|
1497
|
-
const hitStepLimitWithoutAnswer = !wasAborted && step >= maxSteps && !finalText;
|
|
1498
|
-
if (wasAborted && !finalText) {
|
|
1499
|
-
// Never the step-cap text for a turn the budget killed: prefer
|
|
1500
|
-
// the prose the model already produced, else an honest message
|
|
1501
|
-
// naming the actual exit cause.
|
|
1502
|
-
logger.warn(`[GoogleAIStudio] Generate tool call loop ended mid-turn ` +
|
|
1503
|
-
`(${hitTimeLimit() ? "turn time limit" : turnClock.stalled ? "stall watchdog" : "caller abort"}); ` +
|
|
1504
|
-
`returning gathered text or an honest terminal message.`);
|
|
1505
|
-
finalText =
|
|
1506
|
-
lastStepText ||
|
|
1507
|
-
buildLoopExitMessage({
|
|
1508
|
-
timedOut: hitTimeLimit(),
|
|
1509
|
-
stalled: turnClock.stalled,
|
|
1510
|
-
elapsedMs: turnClock.elapsedMs(),
|
|
1511
|
-
wasAborted,
|
|
1512
|
-
...(options.stallTimeoutMs !== undefined
|
|
1513
|
-
? { stallTimeoutMs: options.stallTimeoutMs }
|
|
1514
|
-
: {}),
|
|
1515
|
-
maxSteps,
|
|
1516
|
-
toolCallCount: allToolCalls.length,
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
else {
|
|
1520
|
-
finalText = handleMaxStepsTermination("[GoogleAIStudio]", step, maxSteps, finalText, lastStepText);
|
|
1521
1976
|
}
|
|
1522
|
-
|
|
1523
|
-
//
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
// billed at the cheaper cacheRead rate without double-counting.
|
|
1551
|
-
const adjustedInputTokens = Math.max(0, totalInputTokens - totalCacheReadTokens);
|
|
1552
|
-
const baseResult = {
|
|
1553
|
-
content: finalText,
|
|
1554
|
-
provider: this.providerName,
|
|
1555
|
-
model: modelName,
|
|
1556
|
-
// createAnalytics reads these off the result, so setting them
|
|
1557
|
-
// here is also what puts stepsUsed / stopReason / elapsedMs /
|
|
1558
|
-
// rawFinishReason on `result.analytics`.
|
|
1559
|
-
stopReason,
|
|
1560
|
-
stepsUsed: stepsTaken,
|
|
1561
|
-
...(lastFinishReason !== undefined
|
|
1562
|
-
? { rawFinishReason: lastFinishReason }
|
|
1563
|
-
: {}),
|
|
1977
|
+
// The native turn's own usage (cacheReadTokens / reasoning / the
|
|
1978
|
+
// pre-middleware total) — `v3Result.usage` only ever carries
|
|
1979
|
+
// input/output, so without folding this in, a `wrapGenerate` that
|
|
1980
|
+
// never touches usage at all still silently drops the cache and
|
|
1981
|
+
// reasoning telemetry that reached `enhanceResult` before this
|
|
1982
|
+
// middleware bridge existed. Absent (full short-circuit, no real
|
|
1983
|
+
// turn) degrades to today's input/output-only shape.
|
|
1984
|
+
const nativeUsage = turnBaseResult?.usage;
|
|
1985
|
+
const finalInputTokens = v3Result.usage.inputTokens.total ?? 0;
|
|
1986
|
+
const finalOutputTokens = v3Result.usage.outputTokens.total ?? 0;
|
|
1987
|
+
const finalBaseResult = {
|
|
1988
|
+
...(turnBaseResult ?? {
|
|
1989
|
+
provider: this.providerName,
|
|
1990
|
+
model: modelName,
|
|
1991
|
+
stopReason: resolveTurnStopReason({
|
|
1992
|
+
timedOut: false,
|
|
1993
|
+
stalled: false,
|
|
1994
|
+
wasAborted: false,
|
|
1995
|
+
cappedWithoutAnswer: false,
|
|
1996
|
+
finishReason: v3Result.finishReason.unified,
|
|
1997
|
+
}),
|
|
1998
|
+
stepsUsed: 0,
|
|
1999
|
+
responseTime: Date.now() - startTime,
|
|
2000
|
+
toolsUsed: [],
|
|
2001
|
+
toolExecutions: [],
|
|
2002
|
+
enhancedWithTools: false,
|
|
2003
|
+
}),
|
|
2004
|
+
content: finalContent,
|
|
1564
2005
|
usage: {
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
total: adjustedInputTokens +
|
|
1572
|
-
totalCacheReadTokens +
|
|
1573
|
-
totalOutputTokens +
|
|
1574
|
-
totalReasoningTokens,
|
|
1575
|
-
...(totalCacheReadTokens > 0
|
|
1576
|
-
? { cacheReadTokens: totalCacheReadTokens }
|
|
1577
|
-
: {}),
|
|
1578
|
-
...(totalReasoningTokens > 0
|
|
1579
|
-
? { reasoning: totalReasoningTokens }
|
|
1580
|
-
: {}),
|
|
2006
|
+
...nativeUsage,
|
|
2007
|
+
input: finalInputTokens,
|
|
2008
|
+
output: finalOutputTokens,
|
|
2009
|
+
total: finalInputTokens +
|
|
2010
|
+
finalOutputTokens +
|
|
2011
|
+
(nativeUsage?.cacheReadTokens ?? 0),
|
|
1581
2012
|
},
|
|
1582
|
-
...(totalReasoningTokens > 0 && {
|
|
1583
|
-
reasoningTokens: totalReasoningTokens,
|
|
1584
|
-
}),
|
|
1585
|
-
responseTime,
|
|
1586
|
-
toolsUsed: allToolCalls.map((tc) => tc.toolName),
|
|
1587
|
-
toolExecutions: resolveToolExecutionRecords(options, toolExecutions),
|
|
1588
|
-
enhancedWithTools: allToolCalls.length > 0,
|
|
1589
2013
|
};
|
|
1590
|
-
return this.enhanceResult(
|
|
2014
|
+
return this.enhanceResult(finalBaseResult, options, startTime);
|
|
1591
2015
|
}
|
|
1592
2016
|
finally {
|
|
1593
2017
|
releaseTurnResources();
|