@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/README.md +1 -1
  3. package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
  4. package/dist/browser/neurolink.min.js +437 -435
  5. package/dist/constants/enums.d.ts +237 -0
  6. package/dist/constants/enums.js +257 -0
  7. package/dist/neurolink.js +52 -21
  8. package/dist/providers/catalog/ai21.json +142 -0
  9. package/dist/providers/catalog/aionlabs.json +144 -0
  10. package/dist/providers/catalog/atlas-cloud.json +203 -0
  11. package/dist/providers/catalog/avian-io.json +227 -0
  12. package/dist/providers/catalog/crusoe.json +229 -0
  13. package/dist/providers/catalog/index.generated.d.ts +1 -1
  14. package/dist/providers/catalog/index.generated.js +60 -0
  15. package/dist/providers/catalog/minimax.json +200 -0
  16. package/dist/providers/catalog/moonshot-ai.json +135 -0
  17. package/dist/providers/catalog/nebius.json +165 -0
  18. package/dist/providers/catalog/prime-intellect.json +217 -0
  19. package/dist/providers/catalog/scaleway.json +177 -0
  20. package/dist/providers/catalog/siliconflow.json +242 -0
  21. package/dist/providers/catalog/stepfun.json +155 -0
  22. package/dist/providers/catalog/subconscious.json +93 -0
  23. package/dist/providers/catalog/tinfoil.json +155 -0
  24. package/dist/providers/catalog/umans-ai.json +174 -0
  25. package/dist/providers/catalog/venice-ai.json +208 -0
  26. package/dist/providers/catalog/vultr-inference.json +220 -0
  27. package/dist/providers/catalog/wafer.json +165 -0
  28. package/dist/providers/catalog/wandb-inference.json +249 -0
  29. package/dist/providers/catalog/z-ai.json +255 -0
  30. package/dist/providers/googleAiStudio/client.js +687 -263
  31. package/dist/providers/googleNativeGemini3/utils.d.ts +44 -1
  32. package/dist/providers/googleNativeGemini3/utils.js +300 -1
  33. package/dist/types/providerCatalog.generated.d.ts +2 -2
  34. package/dist/types/providers.d.ts +80 -0
  35. package/docs-site/static/search-index.json +227 -3
  36. 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
- // Run the agentic loop in the background without awaiting it here,
780
- // so we can return the StreamResult (with channel.iterable) immediately.
781
- const loopPromise = (async () => {
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
- // Suppress unhandled-rejection warnings on loopPromise — errors are
1150
- // forwarded to the channel and will surface when the caller iterates.
1151
- loopPromise.catch(() => undefined);
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: channel.iterable,
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
- let finalText = "";
1317
- let lastStepText = "";
1318
- let totalInputTokens = 0;
1319
- let totalOutputTokens = 0;
1320
- let totalCacheReadTokens = 0;
1321
- let totalReasoningTokens = 0;
1322
- const allToolCalls = [];
1323
- const toolExecutions = [];
1324
- let step = 0;
1325
- // Model calls made — see the streaming twin for why this is not
1326
- // `step`.
1327
- let stepsTaken = 0;
1328
- let wasAborted = false;
1329
- let lastFinishReason;
1330
- // Cheap reclaim trigger — see the stream twin.
1331
- const contextGuard = createContextGuard(getContextWindowSize("googleAiStudio", modelName));
1332
- // Agentic loop for tool calling
1333
- // Same shared engine as the streaming twin. This path has no
1334
- // consumer channel — generate() returns one result rather than
1335
- // streaming — so the engine's stream is drained and discarded, and
1336
- // the turn's text comes from the result.
1337
- const baseAdapter = createGeminiLoopAdapter({
1338
- providerLabel: "GoogleAIStudio",
1339
- maxSteps,
1340
- toolFailureBreaker: { maxRetries: DEFAULT_TOOL_MAX_RETRIES },
1341
- liveTools: options.tools ?? {},
1342
- toolGuards: {
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
- ...(declarationsResult ? { declarations: declarationsResult } : {}),
1348
- buildRequest: (contents) => ({
1349
- model: modelName,
1350
- contents,
1351
- config,
1352
- }),
1353
- // See the streaming twin: `config.abortSignal` is the only
1354
- // cancellation channel the SDK reads.
1355
- sendStep: async (request, signal) => {
1356
- turnClock.noteProgress();
1357
- const built = request;
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
- const working = [...contents];
1371
- if (!reclaimAiStudioContext(working, modelName, contextGuard.projectedNextPromptTokens)) {
1372
- return undefined;
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
- contextGuard.resetAfterReclaim();
1375
- return { conversation: working };
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
- lastStepText = stepResult.text || lastStepText;
1399
- for (const result of toolResults) {
1400
- toolExecutions.push({
1401
- name: result.name,
1402
- input: result.args,
1403
- output: result.output,
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
- if (toolResults.length > 0) {
1407
- const stepThoughtSig = extractThoughtSignature(stepResult.raw.rawResponseParts);
1408
- withTimeout(this.handleToolExecutionStorage(stepResult.toolCalls.map((call, i) => ({
1409
- toolName: call.name,
1410
- args: call.args,
1411
- ...(i === 0 && stepThoughtSig
1412
- ? { thoughtSignature: stepThoughtSig }
1413
- : {}),
1414
- stepIndex: step,
1415
- })), toolResults.map((result) => ({
1416
- toolName: result.name,
1417
- output: result.output,
1418
- stepIndex: step,
1419
- })), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
1420
- logger.warn("[GoogleAIStudio] Failed to store native tool executions", {
1421
- error: error instanceof Error ? error.message : String(error),
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
- const next = baseAdapter.buildToolResultMessages(contents, stepResult, toolResults, engineStep);
1426
- // Wrap-up nudge — see the streaming twin.
1427
- if (turnClock.shouldNudgeWrapup()) {
1428
- const last = next[next.length - 1];
1429
- if (last && Array.isArray(last.parts)) {
1430
- last.parts.push({ text: buildWrapupNudgeText(false) });
1431
- }
1432
- }
1433
- try {
1434
- const appended = next[next.length - 1];
1435
- contextGuard.noteAppendedChars(JSON.stringify(appended?.parts ?? []).length);
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
- catch {
1438
- /* estimation is best-effort — never break the loop */
1935
+ else {
1936
+ delete effectiveConfig.systemInstruction;
1439
1937
  }
1440
- return next;
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
- // Same dedup routing as the streaming twin above.
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
- engineResult = await resultPromise;
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
- turnFailure = error;
1963
+ throw this.handleProviderError(error);
1469
1964
  }
1470
- await drainDetachedPump(drain, "GoogleAIStudio");
1471
- if (turnFailure !== undefined) {
1472
- // Same split as the streaming twin: a blown time budget or a
1473
- // caller abort ends the turn honestly, a provider failure still
1474
- // throws.
1475
- if (turnClock.expired || options.abortSignal?.aborted) {
1476
- wasAborted = true;
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
- const responseTime = Date.now() - startTime;
1523
- // Turn-exit discriminator — see the streaming twin.
1524
- const stopReason = resolveTurnStopReason({
1525
- timedOut: hitTimeLimit(),
1526
- stalled: turnClock.stalled,
1527
- wasAborted,
1528
- cappedWithoutAnswer: hitStepLimitWithoutAnswer,
1529
- finishReason: mapGeminiFinishReason(lastFinishReason),
1530
- });
1531
- if (stopReason !== "completed") {
1532
- this.emitTurnEvent({
1533
- phase: stopReason,
1534
- step: stepsTaken,
1535
- maxSteps,
1536
- toolCallCount: allToolCalls.length,
1537
- elapsedMs: turnClock.elapsedMs(),
1538
- });
1539
- }
1540
- // Set token usage and finish reason on the span
1541
- span.setAttribute(ATTR.GEN_AI_INPUT_TOKENS, totalInputTokens);
1542
- span.setAttribute(ATTR.GEN_AI_OUTPUT_TOKENS, totalOutputTokens);
1543
- span.setAttribute(ATTR.GEN_AI_FINISH_REASON, step >= maxSteps ? "max_steps" : "stop");
1544
- // Build EnhancedGenerateResult and route through enhanceResult so
1545
- // analytics / evaluation / tracing stay attached. The native AI
1546
- // Studio generate path bypasses BaseProvider.generate(), so
1547
- // skipping enhanceResult would silently drop those features.
1548
- // Gemini promptTokenCount is OVERLAPPING (already includes
1549
- // cachedContentTokenCount). Subtract once so the cached portion is
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
- input: adjustedInputTokens,
1566
- // Thinking tokens are billed at the output rate but Gemini
1567
- // does NOT include them in candidatesTokenCount, so they are
1568
- // folded into `output` — what calculateCost bills at the
1569
- // output rate — with `reasoning` as the subset.
1570
- output: totalOutputTokens + totalReasoningTokens,
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(baseResult, options, startTime);
2014
+ return this.enhanceResult(finalBaseResult, options, startTime);
1591
2015
  }
1592
2016
  finally {
1593
2017
  releaseTurnResources();