@juspay/neurolink 12.14.15 → 12.14.16

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.
@@ -1406,8 +1406,10 @@ export class GoogleVertexProvider extends BaseProvider {
1406
1406
  if (effectiveSystemPrompt) {
1407
1407
  config.systemInstruction = effectiveSystemPrompt;
1408
1408
  }
1409
- // Add thinking config for Gemini 3
1410
- const nativeThinkingConfig = createNativeThinkingConfig(options.thinkingConfig);
1409
+ // Add thinking config. Gemini 3 takes `thinkingLevel` directly; Gemini
1410
+ // 2.5 rejects that field and needs a translated `thinkingBudget` instead
1411
+ // — createNativeThinkingConfig picks the wire shape from modelName.
1412
+ const nativeThinkingConfig = createNativeThinkingConfig(options.thinkingConfig, modelName);
1411
1413
  if (nativeThinkingConfig) {
1412
1414
  config.thinkingConfig = nativeThinkingConfig;
1413
1415
  }
@@ -2216,8 +2218,10 @@ export class GoogleVertexProvider extends BaseProvider {
2216
2218
  if (effectiveSystemPrompt) {
2217
2219
  config.systemInstruction = effectiveSystemPrompt;
2218
2220
  }
2219
- // Add thinking config for Gemini 3
2220
- const nativeThinkingConfig2 = createNativeThinkingConfig(options.thinkingConfig);
2221
+ // Add thinking config. Gemini 3 takes `thinkingLevel` directly; Gemini
2222
+ // 2.5 rejects that field and needs a translated `thinkingBudget` instead
2223
+ // — createNativeThinkingConfig picks the wire shape from modelName.
2224
+ const nativeThinkingConfig2 = createNativeThinkingConfig(options.thinkingConfig, modelName);
2221
2225
  if (nativeThinkingConfig2) {
2222
2226
  config.thinkingConfig = nativeThinkingConfig2;
2223
2227
  }
@@ -2883,6 +2887,72 @@ export class GoogleVertexProvider extends BaseProvider {
2883
2887
  * Execute stream using native @anthropic-ai/vertex-sdk for Claude models on Vertex AI
2884
2888
  * This bypasses @ai-sdk/google-vertex completely and uses Anthropic's native SDK
2885
2889
  */
2890
+ /**
2891
+ * Extended thinking for Claude-on-Vertex.
2892
+ *
2893
+ * The direct Anthropic provider has always translated `thinkingConfig` into
2894
+ * the Messages API's `thinking` field (anthropic/client.ts). This path never
2895
+ * did — it built the request without one — so `thinkingConfig` was accepted
2896
+ * and silently ignored here: no error, `usage.reasoning` 0, no reasoning
2897
+ * content. Measured against the raw @anthropic-ai/vertex-sdk with the same
2898
+ * project, region, credentials and model, `thinking` is supported and
2899
+ * returns a real thinking block, so the capability was ours to send.
2900
+ *
2901
+ * `thinkingLevel` is deliberately not mapped to a budget: the option's
2902
+ * contract reserves `budgetTokens` for Anthropic models and `thinkingLevel`
2903
+ * for Gemini 3, and inventing a conversion here would be guessing at the
2904
+ * caller's intent.
2905
+ *
2906
+ * The budget must leave room inside `max_tokens` for an answer, so it is
2907
+ * clamped below the ceiling rather than passed through — an over-large
2908
+ * budget is a 400 from the API.
2909
+ *
2910
+ * `useFinalResultTool` (schema / structured-output mode) always pairs with
2911
+ * a forced `tool_choice:{type:"any"}` in the request built right after this
2912
+ * call — Anthropic hard-rejects `thinking` combined with a `tool_choice`
2913
+ * that forces tool use: a live 400, "Thinking may not be enabled when
2914
+ * tool_choice forces tool use." So `thinking` is omitted entirely in that
2915
+ * mode, preserving the prior (pre-thinking-fix) behaviour of silently
2916
+ * ignoring `thinkingConfig` for schema calls instead of hard-failing them —
2917
+ * mirrors the same guard already applied to the OAuth proxy path in
2918
+ * `src/lib/proxy/oauthFetch.ts` (`delete parsed.thinking` when
2919
+ * `tool_choice.type` is `"any"` or `"tool"`). The step/context-cap
2920
+ * finalization backstop reuses this same omission for free since it spreads
2921
+ * `...requestParams`. `tool_choice:{type:"none"}` (the separate
2922
+ * tools-disabled backstop, used only outside schema mode) does not force
2923
+ * tool use — it forces the opposite — so it is not gated here, consistent
2924
+ * with oauthFetch.ts leaving `"none"` alone too.
2925
+ */
2926
+ buildClaudeThinkingParam(
2927
+ // Structural, not TextGenerationOptions: the streaming caller passes
2928
+ // StreamOptions, and only these two fields are read.
2929
+ options, maxTokens, useFinalResultTool) {
2930
+ if (useFinalResultTool) {
2931
+ if (options.thinkingConfig?.enabled) {
2932
+ logger.debug("[GoogleVertex] Omitting thinking: schema mode forces tool_choice:any, which Anthropic rejects alongside thinking");
2933
+ }
2934
+ return undefined;
2935
+ }
2936
+ const budget = options.thinkingConfig?.budgetTokens;
2937
+ if (!options.thinkingConfig?.enabled || !budget) {
2938
+ return undefined;
2939
+ }
2940
+ // Two independent floors, checked explicitly rather than folded into one
2941
+ // Math.max/min expression: Anthropic's own 1024 minimum on budget_tokens,
2942
+ // and this function's 1024-token reservation for the answer that follows
2943
+ // thinking. When maxTokens leaves no room for both — 1025..2047 is the
2944
+ // narrow band where that happens — there is no valid budget, so thinking
2945
+ // is omitted rather than silently coerced to exactly 1024 regardless of
2946
+ // what the caller asked for.
2947
+ if (maxTokens <= 1024) {
2948
+ return undefined;
2949
+ }
2950
+ const clamped = Math.min(budget, maxTokens - 1024);
2951
+ if (clamped < 1024) {
2952
+ return undefined;
2953
+ }
2954
+ return { type: "enabled", budget_tokens: clamped };
2955
+ }
2886
2956
  async executeNativeAnthropicStream(options) {
2887
2957
  const modelName = toVertexAnthropicModelId(options.model || this.modelName || "claude-sonnet-4-5@20250929");
2888
2958
  const startTime = Date.now();
@@ -3102,13 +3172,16 @@ export class GoogleVertexProvider extends BaseProvider {
3102
3172
  }),
3103
3173
  ...(options.topP !== undefined && { topP: options.topP }),
3104
3174
  }, "vertex.anthropic.stream");
3175
+ const streamMaxTokens = resolveClaudeMaxTokens(modelName, options.maxTokens);
3176
+ const streamThinking = this.buildClaudeThinkingParam(options, streamMaxTokens, useFinalResultTool);
3105
3177
  const requestParams = {
3106
3178
  model: modelName,
3107
3179
  // Default to the model's real output ceiling (e.g. 64K for Sonnet 4.x)
3108
3180
  // instead of the legacy 4096, which silently truncated large structured
3109
3181
  // responses mid-JSON. resolveClaudeMaxTokens also clamps over-large
3110
3182
  // caller values so the native Vertex path never 400s.
3111
- max_tokens: resolveClaudeMaxTokens(modelName, options.maxTokens),
3183
+ max_tokens: streamMaxTokens,
3184
+ ...(streamThinking && { thinking: streamThinking }),
3112
3185
  messages: messages,
3113
3186
  ...(tools && tools.length > 0 && { tools }),
3114
3187
  ...(useFinalResultTool && { tool_choice: { type: "any" } }),
@@ -3606,6 +3679,9 @@ export class GoogleVertexProvider extends BaseProvider {
3606
3679
  });
3607
3680
  const pump = (async () => {
3608
3681
  for await (const chunk of engineStream) {
3682
+ if (chunk.reasoning) {
3683
+ channel.push({ content: "", reasoning: chunk.reasoning });
3684
+ }
3609
3685
  if (chunk.content) {
3610
3686
  channel.push({ content: chunk.content });
3611
3687
  liveTextPushedLength += chunk.content.length;
@@ -3636,6 +3712,12 @@ export class GoogleVertexProvider extends BaseProvider {
3636
3712
  if (engineResult) {
3637
3713
  usage.input += engineResult.usage.inputTokens;
3638
3714
  usage.output += engineResult.usage.outputTokens;
3715
+ // A SUBSET already included in outputTokens above — added here
3716
+ // only for observability, never folded into input/output/total.
3717
+ if (engineResult.usage.reasoningTokens) {
3718
+ usage.reasoning =
3719
+ (usage.reasoning ?? 0) + engineResult.usage.reasoningTokens;
3720
+ }
3639
3721
  finishReasonRef.value =
3640
3722
  engineResult.rawStopReason ?? finishReasonRef.value;
3641
3723
  // NOT `toolCalls.length === 0`: that array accumulates across the
@@ -3841,9 +3923,22 @@ export class GoogleVertexProvider extends BaseProvider {
3841
3923
  tools,
3842
3924
  messages: currentMessages,
3843
3925
  });
3926
+ // budget_tokens must stay below max_tokens or Anthropic 400s
3927
+ // the call; terminalMaxTokens can land far below the ceiling
3928
+ // streamThinking was sized against (the context guard can
3929
+ // shrink it well under the original request), so the budget
3930
+ // is rebuilt for the shrunk limit rather than inherited
3931
+ // through the spread below.
3932
+ const backstopThinking = this.buildClaudeThinkingParam(options, terminalMaxTokens, false);
3844
3933
  const backstopStream = await client.messages.stream({
3845
3934
  ...requestParams,
3846
3935
  max_tokens: terminalMaxTokens,
3936
+ // Assigned outright, never a conditional spread: spreading
3937
+ // requestParams above may already carry a `thinking` sized
3938
+ // for the original max_tokens, and only adding a key when
3939
+ // backstopThinking is truthy would leave that stale value
3940
+ // in place instead of clearing it.
3941
+ thinking: backstopThinking,
3847
3942
  tool_choice: { type: "none" },
3848
3943
  system: cachedBackstop.system,
3849
3944
  ...(cachedBackstop.tools &&
@@ -3874,7 +3969,28 @@ export class GoogleVertexProvider extends BaseProvider {
3874
3969
  usage.output +
3875
3970
  turnCacheUsage.read +
3876
3971
  turnCacheUsage.creation;
3972
+ // A SUBSET already included in usage.output above (Anthropic
3973
+ // bills thinking as part of output_tokens) — additive only,
3974
+ // matching the main loop's own usage.reasoning accumulation.
3975
+ if (response.usage?.output_tokens_details?.thinking_tokens) {
3976
+ usage.reasoning =
3977
+ (usage.reasoning ?? 0) +
3978
+ response.usage.output_tokens_details.thinking_tokens;
3979
+ }
3877
3980
  lastStopReason = response.stop_reason;
3981
+ // Extract<> rather than a hand-written object shape: the
3982
+ // real SDK's ThinkingBlock also requires `signature`, which
3983
+ // a literal `{ type: "thinking"; thinking: string }` predicate
3984
+ // omits — TS then rejects the predicate as not assignable to
3985
+ // ContentBlock. Extracting the union member sidesteps that
3986
+ // without hard-coding its full shape here.
3987
+ const backstopReasoning = response.content
3988
+ .filter((block) => block.type === "thinking")
3989
+ .map((block) => block.thinking)
3990
+ .join("");
3991
+ if (backstopReasoning) {
3992
+ channel.push({ content: "", reasoning: backstopReasoning });
3993
+ }
3878
3994
  const backstopText = response.content
3879
3995
  .filter((block) => block.type === "text")
3880
3996
  .map((b) => b.text)
@@ -4325,10 +4441,13 @@ export class GoogleVertexProvider extends BaseProvider {
4325
4441
  }),
4326
4442
  ...(options.topP !== undefined && { topP: options.topP }),
4327
4443
  }, "vertex.anthropic.generate");
4444
+ const generateMaxTokens = resolveClaudeMaxTokens(modelName, options.maxTokens);
4445
+ const generateThinking = this.buildClaudeThinkingParam(options, generateMaxTokens, useFinalResultTool);
4328
4446
  const requestParams = {
4329
4447
  model: modelName,
4330
4448
  // Default to the model's real output ceiling (see stream path note).
4331
- max_tokens: resolveClaudeMaxTokens(modelName, options.maxTokens),
4449
+ max_tokens: generateMaxTokens,
4450
+ ...(generateThinking && { thinking: generateThinking }),
4332
4451
  messages,
4333
4452
  ...(tools && tools.length > 0 && { tools }),
4334
4453
  ...(useFinalResultTool && { tool_choice: { type: "any" } }),
@@ -4371,6 +4490,16 @@ export class GoogleVertexProvider extends BaseProvider {
4371
4490
  // ~1.25x cache-write tiers instead of billing everything at full input rate.
4372
4491
  let totalCacheReadTokens = 0;
4373
4492
  let totalCacheCreationTokens = 0;
4493
+ // A SUBSET already included in totalOutputTokens above (Anthropic bills
4494
+ // thinking tokens as part of output_tokens) — reported separately for
4495
+ // observability, never folded into the input/output/total rollup.
4496
+ let totalReasoningTokens = 0;
4497
+ // Reasoning text has no tool-triggered accumulation path the way
4498
+ // `accumulatedStepText` does (buildToolResultMessages only fires for
4499
+ // steps with tool calls), so it is captured directly from the engine's
4500
+ // per-chunk stream below — otherwise a plain one-shot thinking answer
4501
+ // never surfaces its reasoning text at all.
4502
+ let accumulatedReasoningText = "";
4374
4503
  // Track the final Anthropic stop_reason so we can surface finishReason
4375
4504
  // (notably "length" on token truncation) — the legacy native path always
4376
4505
  // reported "stop", hiding truncation from callers.
@@ -4601,15 +4730,30 @@ export class GoogleVertexProvider extends BaseProvider {
4601
4730
  // engine's own per-tool bound cannot undercut it.
4602
4731
  toolTimeoutMs: toolExecTimeoutMs,
4603
4732
  });
4604
- // Drained and discarded: generate() returns one result rather than
4605
- // streaming, and the per-step text is accumulated in
4733
+ // Text chunks are discarded here: generate() returns one result rather
4734
+ // than streaming, and the per-step text is accumulated in
4606
4735
  // buildToolResultMessages instead — appendStepText joins steps with a
4607
4736
  // NEWLINE, which a chunk-by-chunk `+=` here would silently drop, running
4608
- // consecutive steps' text together. The drain still has to happen:
4609
- // leaving the channel unread stalls the engine once its buffer fills.
4737
+ // consecutive steps' text together. Reasoning has no such step-boundary
4738
+ // hook (buildToolResultMessages only runs for steps WITH tool calls, so
4739
+ // a plain one-shot thinking answer never reaches it), so it is captured
4740
+ // directly off the chunk stream instead. The drain still has to happen
4741
+ // regardless: leaving the channel unread stalls the engine once its
4742
+ // buffer fills.
4743
+ //
4744
+ // The plain `+=` below is deliberate, not the same gap as the text path:
4745
+ // each `chunk.reasoning` here is one `thinking_delta` from inside a
4746
+ // single step (loopAdapter pushes one per delta, not one per step), so
4747
+ // joining on a NEWLINE the way appendStepText does would insert a break
4748
+ // before nearly every delta and fragment one coherent thinking block
4749
+ // into one line per token. Concatenation across steps is a known,
4750
+ // accepted blur — there is no per-step boundary on this channel to join
4751
+ // on — while concatenation within a step is exactly what the API sent.
4610
4752
  const pump = (async () => {
4611
4753
  for await (const chunk of engineStream) {
4612
- void chunk;
4754
+ if (chunk.reasoning) {
4755
+ accumulatedReasoningText += chunk.reasoning;
4756
+ }
4613
4757
  }
4614
4758
  })();
4615
4759
  let engineResult;
@@ -4638,6 +4782,7 @@ export class GoogleVertexProvider extends BaseProvider {
4638
4782
  totalOutputTokens += engineResult.usage.outputTokens;
4639
4783
  totalCacheReadTokens += engineResult.usage.cacheReadTokens ?? 0;
4640
4784
  totalCacheCreationTokens += engineResult.usage.cacheWriteTokens ?? 0;
4785
+ totalReasoningTokens += engineResult.usage.reasoningTokens ?? 0;
4641
4786
  lastStopReason = engineResult.rawStopReason ?? lastStopReason;
4642
4787
  finalText = engineResult.text || finalText;
4643
4788
  // Replace in place: the terminal block and the finalization call both
@@ -4796,9 +4941,22 @@ export class GoogleVertexProvider extends BaseProvider {
4796
4941
  tools,
4797
4942
  messages: currentMessages,
4798
4943
  });
4944
+ // budget_tokens must stay below max_tokens or Anthropic 400s the
4945
+ // call; terminalMaxTokens can land far below the ceiling
4946
+ // generateThinking was sized against (the context guard can
4947
+ // shrink it well under the original request), so the budget is
4948
+ // rebuilt for the shrunk limit rather than inherited through the
4949
+ // spread below.
4950
+ const backstopThinking = this.buildClaudeThinkingParam(options, terminalMaxTokens, false);
4799
4951
  const response = await withTimeout(client.messages.create({
4800
4952
  ...requestParams,
4801
4953
  max_tokens: terminalMaxTokens,
4954
+ // Assigned outright, never a conditional spread: spreading
4955
+ // requestParams above may already carry a `thinking` sized
4956
+ // for the original max_tokens, and only adding a key when
4957
+ // backstopThinking is truthy would leave that stale value
4958
+ // in place instead of clearing it.
4959
+ thinking: backstopThinking,
4802
4960
  tool_choice: { type: "none" },
4803
4961
  system: cachedBackstop.system,
4804
4962
  ...(cachedBackstop.tools &&
@@ -4813,7 +4971,25 @@ export class GoogleVertexProvider extends BaseProvider {
4813
4971
  response.usage?.cache_read_input_tokens || 0;
4814
4972
  totalCacheCreationTokens +=
4815
4973
  response.usage?.cache_creation_input_tokens || 0;
4974
+ // A SUBSET already included in totalOutputTokens above
4975
+ // (Anthropic bills thinking as part of output_tokens) —
4976
+ // additive only, matching the main loop's own accumulation.
4977
+ totalReasoningTokens +=
4978
+ response.usage?.output_tokens_details?.thinking_tokens ?? 0;
4816
4979
  lastStopReason = response.stop_reason;
4980
+ // Extract<> rather than a hand-written object shape: the real
4981
+ // SDK's ThinkingBlock also requires `signature`, which a literal
4982
+ // `{ type: "thinking"; thinking: string }` predicate omits — TS
4983
+ // then rejects the predicate as not assignable to ContentBlock.
4984
+ // Extracting the union member sidesteps that without
4985
+ // hard-coding its full shape here.
4986
+ const backstopReasoning = response.content
4987
+ .filter((block) => block.type === "thinking")
4988
+ .map((block) => block.thinking)
4989
+ .join("");
4990
+ if (backstopReasoning) {
4991
+ accumulatedReasoningText += backstopReasoning;
4992
+ }
4817
4993
  const backstopText = response.content
4818
4994
  .filter((block) => block.type === "text")
4819
4995
  .map((b) => b.text)
@@ -4927,11 +5103,19 @@ export class GoogleVertexProvider extends BaseProvider {
4927
5103
  ...(totalCacheCreationTokens > 0 && {
4928
5104
  cacheCreationTokens: totalCacheCreationTokens,
4929
5105
  }),
5106
+ // A SUBSET already included in `output` above (Anthropic bills
5107
+ // thinking tokens as part of output_tokens) — reported additively,
5108
+ // never folded into output/total.
5109
+ ...(totalReasoningTokens > 0 && { reasoning: totalReasoningTokens }),
4930
5110
  },
4931
5111
  responseTime,
4932
5112
  toolsUsed: externalToolCalls.map((tc) => tc.toolName),
4933
5113
  toolExecutions: resolveToolExecutionRecords(options, externalToolExecutions),
4934
5114
  enhancedWithTools: externalToolCalls.length > 0,
5115
+ ...(accumulatedReasoningText && { reasoning: accumulatedReasoningText }),
5116
+ ...(totalReasoningTokens > 0 && {
5117
+ reasoningTokens: totalReasoningTokens,
5118
+ }),
4935
5119
  };
4936
5120
  // Route through enhanceResult so analytics/evaluation/tracing are picked
4937
5121
  // up the same way the BaseProvider.generate() path picks them up. The
@@ -6245,6 +6429,17 @@ export class GoogleVertexProvider extends BaseProvider {
6245
6429
  /**
6246
6430
  * Get model suggestions when a model is not found
6247
6431
  */
6432
+ /**
6433
+ * Candidate model ids for a "model not available" error.
6434
+ *
6435
+ * This list is static. It cannot know what the caller's project and region
6436
+ * actually serve, so it must not claim to: it previously announced
6437
+ * "always available" and included the requested model itself, which
6438
+ * produced errors that recommended the very id that had just failed —
6439
+ * `gemini-3-pro-preview-11-2025` suggesting `gemini-3-pro-preview-11-2025`.
6440
+ * The requested model is now filtered out, and the wording says these are
6441
+ * ids to try rather than ids known to work.
6442
+ */
6248
6443
  getModelSuggestions(requestedModel) {
6249
6444
  const availableModels = {
6250
6445
  google: [
@@ -6270,20 +6465,29 @@ export class GoogleVertexProvider extends BaseProvider {
6270
6465
  "claude-3-opus-20240229",
6271
6466
  ],
6272
6467
  };
6273
- let suggestions = "\n🤖 Google Models (always available):\n";
6274
- availableModels.google.forEach((model) => {
6468
+ // Never offer the id that just failed back to the caller. Compared by
6469
+ // base id (before "@") so a versioned request (e.g. "gemini-2.5-pro@002")
6470
+ // still excludes the unversioned list entry it failed against.
6471
+ const requestedBase = (requestedModel ?? "").split("@")[0].toLowerCase();
6472
+ const notRequested = (model) => model.split("@")[0].toLowerCase() !== requestedBase;
6473
+ const google = availableModels.google.filter(notRequested);
6474
+ const claude = availableModels.claude.filter(notRequested);
6475
+ let suggestions = "\n🤖 Google Models (availability depends on your project and region):\n";
6476
+ google.forEach((model) => {
6275
6477
  suggestions += ` • ${model}\n`;
6276
6478
  });
6277
6479
  suggestions += "\n🧠 Claude Models (requires Anthropic integration):\n";
6278
- availableModels.claude.forEach((model) => {
6480
+ claude.forEach((model) => {
6279
6481
  suggestions += ` • ${model}\n`;
6280
6482
  });
6281
- // If the requested model looks like a Claude model, provide specific guidance
6483
+ // If the requested model looks like a Claude model, provide specific guidance.
6484
+ // Worded without requestedModel so the just-failed id isn't echoed back here too.
6282
6485
  if (requestedModel && requestedModel.toLowerCase().includes("claude")) {
6283
- suggestions += `\n💡 Tip: "${requestedModel}" appears to be a Claude model.\n`;
6486
+ suggestions +=
6487
+ "\n💡 Tip: this model id appears to be a Claude model, which just failed.\n";
6284
6488
  suggestions +=
6285
6489
  "Ensure Anthropic integration is enabled in your Google Cloud project.\n";
6286
- suggestions += "Try using an available Claude model from the list above.";
6490
+ suggestions += "Try another Claude model from the list above.";
6287
6491
  }
6288
6492
  return suggestions;
6289
6493
  }
@@ -506,8 +506,18 @@ export type CreateThinkingConfigOptions = {
506
506
  /** Thinking level for Gemini 3 models */
507
507
  thinkingLevel?: ThinkingLevel;
508
508
  };
509
- /** Native SDK thinkingConfig structure for Gemini native SDK. */
509
+ /**
510
+ * Native SDK thinkingConfig structure for Gemini native SDK.
511
+ *
512
+ * `thinkingLevel` and `thinkingBudget` are mutually exclusive on the wire:
513
+ * Gemini 3 models accept `thinkingLevel`; Gemini 2.5 (and earlier) models
514
+ * reject it outright with INVALID_ARGUMENT "thinking_level not supported by
515
+ * this model" and require the numeric `thinkingBudget` instead. Both fields
516
+ * are optional here so a single type can represent either shape — callers
517
+ * building the request pick whichever field the target model accepts.
518
+ */
510
519
  export type NativeThinkingConfig = {
511
520
  includeThoughts: boolean;
512
- thinkingLevel: ThinkingLevel;
521
+ thinkingLevel?: ThinkingLevel;
522
+ thinkingBudget?: number;
513
523
  };
@@ -1394,19 +1394,28 @@ export type TextGenerationOptions = {
1394
1394
  * - Range: 5000-100000 tokens
1395
1395
  * - `thinkingLevel` is ignored for Anthropic
1396
1396
  *
1397
- * **Google Gemini 3 (gemini-3.1-pro-preview, gemini-3-flash-preview):**
1398
- * - Use `thinkingConfig.thinkingLevel` or `thinkingLevel`
1397
+ * **Google Gemini 2.5 and 3 (gemini-2.5-pro/flash/flash-lite,
1398
+ * gemini-3.1-pro-preview, gemini-3-flash-preview), via `thinkingConfig`:**
1399
+ * - Use `thinkingConfig.thinkingLevel` (the public contract is the same
1400
+ * `thinkingLevel` lever for the whole Gemini family)
1399
1401
  * - Levels: minimal, low, medium, high
1400
- * - `budgetTokens` is ignored for Gemini (uses level-based allocation)
1402
+ * - `thinkingConfig.budgetTokens` is not read for Gemini — the wire-level
1403
+ * Vertex/native-SDK request always carries a level-derived value.
1404
+ * Internally, Gemini 3 sends the vendor's `thinkingLevel` field as-is;
1405
+ * Gemini 2.5 has no such field (Vertex rejects it with INVALID_ARGUMENT
1406
+ * "thinking_level not supported by this model") and the requested level
1407
+ * is translated to the vendor's numeric `thinkingBudget` within that
1408
+ * model's verified range instead. That translation is an implementation
1409
+ * detail of the provider — callers keep using `thinkingLevel` either way.
1401
1410
  *
1402
1411
  * ### Option Compatibility Matrix
1403
1412
  *
1404
- * | Option | Anthropic | Gemini 3 | Other Providers |
1405
- * |----------------|-----------|----------|-----------------|
1406
- * | thinking | Yes | Yes | Ignored |
1407
- * | thinkingBudget | Yes | Ignored | Ignored |
1408
- * | thinkingLevel | Ignored | Yes | Ignored |
1409
- * | thinkingConfig | Yes | Yes | Ignored |
1413
+ * | Option | Anthropic | Gemini 2.5 / 3 | Other Providers |
1414
+ * |----------------|-----------|----------------|------------------|
1415
+ * | thinking | Yes | Yes | Ignored |
1416
+ * | thinkingBudget | Yes | Ignored | Ignored |
1417
+ * | thinkingLevel | Ignored | Yes | Ignored |
1418
+ * | thinkingConfig | Yes | Yes | Ignored |
1410
1419
  *
1411
1420
  * ### Examples
1412
1421
  *
@@ -6,6 +6,16 @@ export declare function isGemini25Model(modelName: string): boolean;
6
6
  export declare function supportsThinkingConfig(modelName: string): boolean;
7
7
  export declare function supportsPromptCaching(modelName: string): boolean;
8
8
  export declare function getMaxThinkingBudgetTokens(modelName: string): number;
9
+ /**
10
+ * Resolves the Vertex-accepted `thinkingBudget` bounds for a Gemini 2.5
11
+ * model. Returns undefined for anything that isn't a Gemini 2.5 model —
12
+ * Gemini 3 takes `thinkingLevel` directly, and no thinking-budget mapping
13
+ * has been verified for earlier Gemini generations.
14
+ */
15
+ export declare function getGemini25ThinkingBudgetRange(modelName: string): {
16
+ min: number;
17
+ max: number;
18
+ } | undefined;
9
19
  export declare function getModelFamily(modelName: string): string;
10
20
  /**
11
21
  * Check if a model has restricted output token limit (32768 max)
@@ -58,6 +58,44 @@ export function getMaxThinkingBudgetTokens(modelName) {
58
58
  }
59
59
  return 10000;
60
60
  }
61
+ // Per-model `thinkingBudget` bounds for the Gemini 2.5 family, confirmed
62
+ // against live Vertex generateContent responses (not just vendor docs, which
63
+ // have been observed to drift from runtime behavior — see the "known gotcha"
64
+ // on Gemini 2.5 Pro's range in public bug reports). Vertex rejects a budget
65
+ // outside these bounds with INVALID_ARGUMENT "thinking_budget is out of
66
+ // range; supported values are integers from <min> to <max>".
67
+ //
68
+ // Flash's true floor is 1, but budget:1 measured 0 thoughtsTokenCount — i.e.
69
+ // technically accepted but indistinguishable from thinking being off. The
70
+ // floor is bumped to 128 (Pro's real floor) here so a "minimal" mapping
71
+ // still produces observable thinking. Must precede the plain "flash" pattern
72
+ // below — "gemini-2.5-flash-lite" also matches a bare `/^gemini-2\.5-flash/`.
73
+ const GEMINI_25_THINKING_BUDGET_RANGES = [
74
+ { pattern: /^gemini-2\.5-flash-lite/i, min: 512, max: 24576 },
75
+ { pattern: /^gemini-2\.5-flash/i, min: 128, max: 24576 },
76
+ { pattern: /^gemini-2\.5-pro/i, min: 128, max: 32768 },
77
+ ];
78
+ // Bounds safe for any Gemini 2.5 variant not explicitly listed above — the
79
+ // intersection of all three known models' verified ranges, so a model we
80
+ // haven't named still gets a budget every family member accepts.
81
+ const GEMINI_25_FALLBACK_BUDGET_RANGE = { min: 512, max: 24576 };
82
+ /**
83
+ * Resolves the Vertex-accepted `thinkingBudget` bounds for a Gemini 2.5
84
+ * model. Returns undefined for anything that isn't a Gemini 2.5 model —
85
+ * Gemini 3 takes `thinkingLevel` directly, and no thinking-budget mapping
86
+ * has been verified for earlier Gemini generations.
87
+ */
88
+ export function getGemini25ThinkingBudgetRange(modelName) {
89
+ if (!isValidModelName(modelName) || !isGemini25Model(modelName)) {
90
+ return undefined;
91
+ }
92
+ for (const { pattern, min, max } of GEMINI_25_THINKING_BUDGET_RANGES) {
93
+ if (pattern.test(modelName)) {
94
+ return { min, max };
95
+ }
96
+ }
97
+ return GEMINI_25_FALLBACK_BUDGET_RANGE;
98
+ }
61
99
  export function getModelFamily(modelName) {
62
100
  if (!isValidModelName(modelName)) {
63
101
  return "unknown";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Helpers that keep lazily-populated StreamResult fields (`toolsUsed`,
3
+ * `toolExecutions`, and any other field a provider defines via
4
+ * `Object.defineProperty` with only a `get`) alive as the result object is
5
+ * re-wrapped or narrowed while it travels from a provider's `stream()`
6
+ * implementation up through BaseProvider and NeuroLink to the caller.
7
+ *
8
+ * A plain object spread (`{ ...result }`) or an object literal that reads
9
+ * `result.someField` invokes any getter on `result` immediately and stores
10
+ * the RETURNED VALUE as a static data property on the copy. For a
11
+ * background-loop stream (native Vertex/Anthropic paths, and any
12
+ * OpenAI-compatible provider that appends to `toolExecutionSummaries` while
13
+ * the consumer drains the stream), that snapshot happens before a single
14
+ * chunk has been pulled — so the copy is permanently frozen at empty/undefined
15
+ * even though the source object goes on to reflect real values as the
16
+ * background loop runs. Re-applying the ORIGINAL property descriptor (not
17
+ * the value it currently returns) keeps the copy lazy: every later read on
18
+ * the copy still calls the same getter, closing over the same live backing
19
+ * state.
20
+ */
21
+ /**
22
+ * Re-apply every getter-defined accessor from `source` onto `target`,
23
+ * in place. Plain data properties (arrays copied by reference, primitives)
24
+ * are left untouched — an object spread already preserves those correctly,
25
+ * since copying an array reference keeps observing later `.push()`es.
26
+ */
27
+ export declare function preserveLiveStreamAccessors<T extends object>(source: T, target: T): T;
28
+ /**
29
+ * Build a `PropertyDescriptorMap` carrying the named fields' descriptors
30
+ * (data or accessor) from `source`, for use with `Object.defineProperties`
31
+ * when constructing a NEW, narrower object at a boundary that lists an
32
+ * explicit field set. Reading `source[key]` directly at such a boundary
33
+ * (`target.toolsUsed = source.toolsUsed`) would invoke a getter immediately;
34
+ * copying the descriptor instead defers that read to whoever accesses the
35
+ * field on `target` later. Keys absent from `source` are omitted, matching
36
+ * how an optional field would be omitted from a manual copy.
37
+ */
38
+ export declare function pickLiveStreamDescriptors(source: object, keys: readonly string[]): PropertyDescriptorMap;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Helpers that keep lazily-populated StreamResult fields (`toolsUsed`,
3
+ * `toolExecutions`, and any other field a provider defines via
4
+ * `Object.defineProperty` with only a `get`) alive as the result object is
5
+ * re-wrapped or narrowed while it travels from a provider's `stream()`
6
+ * implementation up through BaseProvider and NeuroLink to the caller.
7
+ *
8
+ * A plain object spread (`{ ...result }`) or an object literal that reads
9
+ * `result.someField` invokes any getter on `result` immediately and stores
10
+ * the RETURNED VALUE as a static data property on the copy. For a
11
+ * background-loop stream (native Vertex/Anthropic paths, and any
12
+ * OpenAI-compatible provider that appends to `toolExecutionSummaries` while
13
+ * the consumer drains the stream), that snapshot happens before a single
14
+ * chunk has been pulled — so the copy is permanently frozen at empty/undefined
15
+ * even though the source object goes on to reflect real values as the
16
+ * background loop runs. Re-applying the ORIGINAL property descriptor (not
17
+ * the value it currently returns) keeps the copy lazy: every later read on
18
+ * the copy still calls the same getter, closing over the same live backing
19
+ * state.
20
+ */
21
+ /**
22
+ * Re-apply every getter-defined accessor from `source` onto `target`,
23
+ * in place. Plain data properties (arrays copied by reference, primitives)
24
+ * are left untouched — an object spread already preserves those correctly,
25
+ * since copying an array reference keeps observing later `.push()`es.
26
+ */
27
+ export function preserveLiveStreamAccessors(source, target) {
28
+ for (const key of Object.getOwnPropertyNames(source)) {
29
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
30
+ if (descriptor && typeof descriptor.get === "function") {
31
+ Object.defineProperty(target, key, descriptor);
32
+ }
33
+ }
34
+ return target;
35
+ }
36
+ /**
37
+ * Build a `PropertyDescriptorMap` carrying the named fields' descriptors
38
+ * (data or accessor) from `source`, for use with `Object.defineProperties`
39
+ * when constructing a NEW, narrower object at a boundary that lists an
40
+ * explicit field set. Reading `source[key]` directly at such a boundary
41
+ * (`target.toolsUsed = source.toolsUsed`) would invoke a getter immediately;
42
+ * copying the descriptor instead defers that read to whoever accesses the
43
+ * field on `target` later. Keys absent from `source` are omitted, matching
44
+ * how an optional field would be omitted from a manual copy.
45
+ */
46
+ export function pickLiveStreamDescriptors(source, keys) {
47
+ const descriptors = {};
48
+ for (const key of keys) {
49
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
50
+ if (descriptor) {
51
+ descriptors[key] = descriptor;
52
+ }
53
+ }
54
+ return descriptors;
55
+ }