@juspay/neurolink 12.41.6 → 12.41.8
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 +2 -2
- package/dist/browser/neurolink.min.js +360 -356
- package/dist/cli/commands/models.js +16 -1
- package/dist/providers/catalog/fireworks.json +4 -3
- package/dist/providers/googleVertex/client.d.ts +217 -4
- package/dist/providers/googleVertex/client.js +650 -56
- package/dist/utils/modelChoices.d.ts +20 -0
- package/dist/utils/modelChoices.js +37 -4
- package/docs-site/static/search-index.json +3 -3
- package/package.json +2 -1
|
@@ -22,6 +22,7 @@ import { FileDetector } from "../../utils/fileDetector.js";
|
|
|
22
22
|
import { mergeMediaFileAliases, normalizeVisionImageFormats, processUnifiedFilesArray, } from "../../utils/messageBuilder.js";
|
|
23
23
|
import { logger } from "../../utils/logger.js";
|
|
24
24
|
import { drainDetachedPump } from "../../utils/drainDetachedPump.js";
|
|
25
|
+
import { releaseIterator } from "../../utils/streamCancellation.js";
|
|
25
26
|
import { fireOnErrorOnce, getLifecycleMiddlewareConfig, isLifecycleStreamCallbacksOwnedByBaseProvider, } from "../../utils/lifecycleCallbacks.js";
|
|
26
27
|
import { GEMINI_ELISION_NOTE, planGeminiLoopReclaim, previewGeminiToolResponseText, } from "../../context/geminiLoopGuard.js";
|
|
27
28
|
import { ANTHROPIC_ELISION_NOTE, planAnthropicLoopReclaim, previewAnthropicToolResultText, } from "../../context/anthropicLoopGuard.js";
|
|
@@ -426,6 +427,120 @@ const createVertexAnthropicSettings = async (region, timeoutMs, direct, baseURL)
|
|
|
426
427
|
const isAnthropicModel = (modelName) => {
|
|
427
428
|
return modelName.toLowerCase().includes("claude");
|
|
428
429
|
};
|
|
430
|
+
/**
|
|
431
|
+
* Bridge a native Vertex stream loop's chunk iterable into a V3
|
|
432
|
+
* `ReadableStream`, so `wrapLanguageModel`'s `doStream` can drive it through
|
|
433
|
+
* the middleware chain. Lazy and pull-based — one native chunk in, one V3
|
|
434
|
+
* part out per `pull()` — because the native Anthropic loop
|
|
435
|
+
* (`executeNativeAnthropicStream`) is genuinely live: it returns its
|
|
436
|
+
* `StreamResult` while a background loop is still generating, with
|
|
437
|
+
* `usage`/`finishReason` as getters that settle only once the loop's
|
|
438
|
+
* channel closes. Eagerly draining the source here would silently turn that
|
|
439
|
+
* real-time stream into collect-then-replay. The Gemini3 loop's chunks are
|
|
440
|
+
* already fully resolved by the time this runs, so the same lazy pull is
|
|
441
|
+
* just as correct there — it has nothing left to wait on.
|
|
442
|
+
*
|
|
443
|
+
* Mirrors `chunksToV3Stream` in `openaiChatCompletionsBase.ts`, which is not
|
|
444
|
+
* exported, hence a local copy adapted to the native `{content, reasoning?}`
|
|
445
|
+
* chunk shape instead of `OpenAICompatStreamChunk`.
|
|
446
|
+
*
|
|
447
|
+
* `cancel()` releases the native iterator without awaiting it
|
|
448
|
+
* (`releaseIterator`), and the not-awaiting is what matters. The native
|
|
449
|
+
* `source` is `createStreamChannel`'s single-flight generator: `.return()`
|
|
450
|
+
* called while a `.next()` is in flight is queued behind it and only runs once
|
|
451
|
+
* that pending `.next()` settles (the next upstream frame, or the loop's own
|
|
452
|
+
* turn clock). A `ReadableStream` with the default `highWaterMark` calls
|
|
453
|
+
* `pull()` ahead of demand, so a `.next()` is routinely outstanding when the
|
|
454
|
+
* consumer breaks. Awaiting `iterator.return()` here would hold open
|
|
455
|
+
* `v3StreamToNativeChunks`'s `reader.cancel()`, and with it the `for await`
|
|
456
|
+
* exit inside `BaseProvider`'s lifecycle wrapper, whose `finally` is what runs
|
|
457
|
+
* `teardown()` — the abort of the call's own signal that actually releases the
|
|
458
|
+
* upstream connection. A held-open upstream would then keep the consumer's
|
|
459
|
+
* `break` parked until a frame or the turn clock arrived. Releasing without
|
|
460
|
+
* waiting lets `teardown()` run at once; the queued `.return()` then completes
|
|
461
|
+
* by itself once that abort settles the pending `.next()`.
|
|
462
|
+
*/
|
|
463
|
+
const nativeChunksToV3Stream = (source, completion) => {
|
|
464
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
465
|
+
return new ReadableStream({
|
|
466
|
+
async pull(controller) {
|
|
467
|
+
try {
|
|
468
|
+
const next = await iterator.next();
|
|
469
|
+
if (next.done) {
|
|
470
|
+
controller.enqueue(completion());
|
|
471
|
+
controller.close();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const chunk = next.value;
|
|
475
|
+
if ("content" in chunk && typeof chunk.content === "string") {
|
|
476
|
+
if ("reasoning" in chunk &&
|
|
477
|
+
typeof chunk.reasoning === "string" &&
|
|
478
|
+
chunk.reasoning.length > 0) {
|
|
479
|
+
controller.enqueue({
|
|
480
|
+
type: "reasoning-delta",
|
|
481
|
+
delta: chunk.reasoning,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
if (chunk.content.length > 0) {
|
|
485
|
+
controller.enqueue({ type: "text-delta", delta: chunk.content });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
controller.error(error);
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
cancel() {
|
|
494
|
+
releaseIterator(iterator);
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
};
|
|
498
|
+
/**
|
|
499
|
+
* Drain a V3 `ReadableStream` (the middleware-wrapped native stream) back
|
|
500
|
+
* into the `{content, reasoning?}` chunk shape `StreamResult.stream`
|
|
501
|
+
* callers already iterate. `onFinish` captures the terminal `finish` part
|
|
502
|
+
* instead of yielding it — native chunk consumers have never seen a
|
|
503
|
+
* finish-shaped chunk and don't expect one.
|
|
504
|
+
*
|
|
505
|
+
* Mirrors `v3StreamToChunks` in `openaiChatCompletionsBase.ts` (also not
|
|
506
|
+
* exported).
|
|
507
|
+
*/
|
|
508
|
+
async function* v3StreamToNativeChunks(stream, onFinish) {
|
|
509
|
+
const reader = stream.getReader();
|
|
510
|
+
let done = false;
|
|
511
|
+
try {
|
|
512
|
+
while (true) {
|
|
513
|
+
const next = await reader.read();
|
|
514
|
+
if (next.done) {
|
|
515
|
+
done = true;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const part = next.value;
|
|
519
|
+
if (part.type === "text-delta") {
|
|
520
|
+
yield { content: part.delta };
|
|
521
|
+
}
|
|
522
|
+
else if (part.type === "reasoning-delta") {
|
|
523
|
+
yield { content: "", reasoning: part.delta };
|
|
524
|
+
}
|
|
525
|
+
else if (part.type === "finish") {
|
|
526
|
+
onFinish(part);
|
|
527
|
+
}
|
|
528
|
+
else if (part.type === "error") {
|
|
529
|
+
throw part.error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
try {
|
|
535
|
+
if (!done) {
|
|
536
|
+
await reader.cancel();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
reader.releaseLock();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
429
544
|
/**
|
|
430
545
|
* Google Vertex AI Provider v2 - BaseProvider Implementation
|
|
431
546
|
*
|
|
@@ -934,7 +1049,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
934
1049
|
model: modelName,
|
|
935
1050
|
totalToolCount: Object.keys(optionTools).length,
|
|
936
1051
|
});
|
|
937
|
-
result = await this.
|
|
1052
|
+
result = await this.runNativeStreamWithMiddleware(mergedOptions, modelName, (effectiveOptions) => this.executeNativeAnthropicStream(effectiveOptions));
|
|
938
1053
|
}
|
|
939
1054
|
else {
|
|
940
1055
|
// ALL Gemini models use native @google/genai SDK
|
|
@@ -942,7 +1057,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
942
1057
|
model: modelName,
|
|
943
1058
|
totalToolCount: Object.keys(optionTools).length,
|
|
944
1059
|
});
|
|
945
|
-
result = await this.
|
|
1060
|
+
result = await this.runNativeStreamWithMiddleware(mergedOptions, modelName, (effectiveOptions) => this.executeNativeGemini3Stream(effectiveOptions));
|
|
946
1061
|
}
|
|
947
1062
|
// Cost / token usage on the stream span. Native streams resolve
|
|
948
1063
|
// usage synchronously (the stream loop has already drained), so
|
|
@@ -2012,6 +2127,44 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2012
2127
|
}
|
|
2013
2128
|
return result;
|
|
2014
2129
|
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Resolve the text a native Vertex *generate* call actually sends, for
|
|
2132
|
+
* both `executeNativeGemini3Generate` and `executeNativeAnthropicGenerate`.
|
|
2133
|
+
*
|
|
2134
|
+
* The baseline here is the same `input.text || prompt || "Please
|
|
2135
|
+
* respond."` chain both call sites have always used — `input.text` wins
|
|
2136
|
+
* when set because the file preprocessors (`processUnifiedFilesArray`,
|
|
2137
|
+
* `processCSVFilesForNativeSDK`) append attached-file content to it while
|
|
2138
|
+
* `options.prompt` stays the pre-attachment snapshot; `prompt` is the
|
|
2139
|
+
* fallback for the legacy prompt-only `generateText()` convention, which
|
|
2140
|
+
* never sets `input` at all; the literal is the last resort when neither
|
|
2141
|
+
* is set.
|
|
2142
|
+
*
|
|
2143
|
+
* That `||` chain alone has a gap model middleware falls into:
|
|
2144
|
+
* `applyTurnParamsRewrite` writes every rewritten turn back onto
|
|
2145
|
+
* `options.input.text`, including an *emptied* one (a redaction). `||`
|
|
2146
|
+
* treats that empty string exactly like an unset `input.text` and falls
|
|
2147
|
+
* through to `options.prompt` — which middleware never touched, since it
|
|
2148
|
+
* is a plain snapshot taken before middleware ran — so a redaction a
|
|
2149
|
+
* middleware applied is silently discarded and the ORIGINAL text still
|
|
2150
|
+
* reaches Vertex.
|
|
2151
|
+
*
|
|
2152
|
+
* The fix is narrow: when `input.text` is *set* (not undefined) and
|
|
2153
|
+
* differs from `prompt`, something deliberately changed it since the
|
|
2154
|
+
* snapshot was taken — a middleware rewrite, or a file preprocessor's
|
|
2155
|
+
* attachment-content append — and that divergence itself is the signal to
|
|
2156
|
+
* trust `input.text` outright, empty string included. When the two agree
|
|
2157
|
+
* (the untouched, no-middleware case, including the STT-silence
|
|
2158
|
+
* `input.text === prompt === ""` placeholder) there is no divergence to
|
|
2159
|
+
* trust, so the original fallback chain still applies unchanged.
|
|
2160
|
+
*/
|
|
2161
|
+
resolveNativeGenerateText(options) {
|
|
2162
|
+
if (options.input?.text !== undefined &&
|
|
2163
|
+
options.input.text !== options.prompt) {
|
|
2164
|
+
return options.input.text;
|
|
2165
|
+
}
|
|
2166
|
+
return options.input?.text || options.prompt || "Please respond.";
|
|
2167
|
+
}
|
|
2015
2168
|
/**
|
|
2016
2169
|
* Execute generate using native @google/genai SDK for Gemini 3 models on Vertex AI
|
|
2017
2170
|
* This bypasses @ai-sdk/google-vertex to properly handle thought_signature
|
|
@@ -2027,7 +2180,8 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2027
2180
|
});
|
|
2028
2181
|
// Build contents from input with multimodal support
|
|
2029
2182
|
// Prioritize input.text over prompt since processCSVFilesForNativeSDK modifies input.text with CSV data
|
|
2030
|
-
|
|
2183
|
+
// (see resolveNativeGenerateText for the full precedence + middleware-redaction rationale)
|
|
2184
|
+
const inputText = this.resolveNativeGenerateText(options);
|
|
2031
2185
|
const contents = [];
|
|
2032
2186
|
// Build user message parts - start with text
|
|
2033
2187
|
const userParts = [{ text: inputText }];
|
|
@@ -4310,8 +4464,9 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4310
4464
|
// attached file was silently dropped on the Claude generate path — the
|
|
4311
4465
|
// model answered "no file attached" to a request that carried one. The
|
|
4312
4466
|
// native Gemini path already reads input.text first for exactly this
|
|
4313
|
-
// reason.
|
|
4314
|
-
|
|
4467
|
+
// reason. (See resolveNativeGenerateText for the full precedence +
|
|
4468
|
+
// middleware-redaction rationale.)
|
|
4469
|
+
const inputText = this.resolveNativeGenerateText(options);
|
|
4315
4470
|
// Replay conversationMessages (with tool turns), else the legacy text-only conversationHistory.
|
|
4316
4471
|
if (options.conversationMessages &&
|
|
4317
4472
|
options.conversationMessages.length > 0) {
|
|
@@ -5513,57 +5668,14 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5513
5668
|
model: modelName,
|
|
5514
5669
|
totalToolCount: Object.keys(mergedOptions.tools).length,
|
|
5515
5670
|
});
|
|
5516
|
-
nativeResult =
|
|
5517
|
-
await this.executeNativeAnthropicGenerate(mergedOptions);
|
|
5671
|
+
nativeResult = await this.runNativeGenerateWithMiddleware(mergedOptions, modelName, (effectiveOptions) => this.executeNativeAnthropicGenerate(effectiveOptions));
|
|
5518
5672
|
}
|
|
5519
5673
|
else {
|
|
5520
5674
|
logger.info("[GoogleVertex] Routing Gemini generate to native @google/genai", {
|
|
5521
5675
|
model: modelName,
|
|
5522
5676
|
totalToolCount: Object.keys(mergedOptions.tools).length,
|
|
5523
5677
|
});
|
|
5524
|
-
|
|
5525
|
-
nativeResult =
|
|
5526
|
-
await this.executeNativeGemini3Generate(mergedOptions);
|
|
5527
|
-
}
|
|
5528
|
-
catch (nativeError) {
|
|
5529
|
-
// Vertex rejects over-constrained responseSchemas with a
|
|
5530
|
-
// deterministic 400 ("too many states" — constrained-decoding
|
|
5531
|
-
// state explosion). Re-sending the same schema can never
|
|
5532
|
-
// succeed, so retry ONCE with the schema dropped but JSON
|
|
5533
|
-
// mode kept: output.format "json" still sets
|
|
5534
|
-
// responseMimeType application/json on the no-tools path,
|
|
5535
|
-
// so the model is forced to emit JSON — just without the
|
|
5536
|
-
// offending schema constraints. The NeuroLink layer then
|
|
5537
|
-
// coerces the JSON text into the caller's original schema
|
|
5538
|
-
// (generate({schema}) guarantee holds).
|
|
5539
|
-
const requestedStructured = mergedOptions.schema !== undefined ||
|
|
5540
|
-
mergedOptions.output?.format === "json";
|
|
5541
|
-
// Tools present → the executor skips responseMimeType, so a
|
|
5542
|
-
// schema-less retry would NOT actually run in JSON mode and
|
|
5543
|
-
// the structured-output contract would silently degrade to
|
|
5544
|
-
// prose. Only the no-tools case retries faithfully; with
|
|
5545
|
-
// tools, surface the 400 to the caller instead.
|
|
5546
|
-
const hasTools = !mergedOptions.disableTools &&
|
|
5547
|
-
Object.keys(mergedOptions.tools ?? {}).length > 0;
|
|
5548
|
-
if (requestedStructured &&
|
|
5549
|
-
!hasTools &&
|
|
5550
|
-
isSchemaComplexityError(nativeError)) {
|
|
5551
|
-
logger.warn("[GoogleVertex] responseSchema too complex for constrained decoding — retrying native generate in schema-less JSON mode", {
|
|
5552
|
-
model: modelName,
|
|
5553
|
-
error: nativeError instanceof Error
|
|
5554
|
-
? nativeError.message
|
|
5555
|
-
: String(nativeError),
|
|
5556
|
-
});
|
|
5557
|
-
nativeResult = await this.executeNativeGemini3Generate({
|
|
5558
|
-
...mergedOptions,
|
|
5559
|
-
schema: undefined,
|
|
5560
|
-
output: { format: "json" },
|
|
5561
|
-
});
|
|
5562
|
-
}
|
|
5563
|
-
else {
|
|
5564
|
-
throw nativeError;
|
|
5565
|
-
}
|
|
5566
|
-
}
|
|
5678
|
+
nativeResult = await this.runNativeGenerateWithMiddleware(mergedOptions, modelName, (effectiveOptions) => this.executeNativeGemini3GenerateWithSchemaFallback(effectiveOptions, modelName));
|
|
5567
5679
|
}
|
|
5568
5680
|
executionSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_OUTPUT, spanJsonAttribute(nativeResult?.content ?? ""));
|
|
5569
5681
|
return nativeResult;
|
|
@@ -5731,15 +5843,484 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5731
5843
|
stream: wrappedIterable,
|
|
5732
5844
|
});
|
|
5733
5845
|
}
|
|
5846
|
+
/**
|
|
5847
|
+
* The text a native Vertex turn is built from before any model middleware
|
|
5848
|
+
* rewrite — `options.input.text` when the caller set it (the normal
|
|
5849
|
+
* `generate()`/`stream()` convention), else `options.prompt` (the legacy
|
|
5850
|
+
* prompt-only `generateText()` convention, where `input` is never
|
|
5851
|
+
* populated — see `generateTextInInstanceScope` in `src/lib/neurolink.ts`,
|
|
5852
|
+
* which validates `options.prompt` and never touches `options.input`).
|
|
5853
|
+
*
|
|
5854
|
+
* `buildTurnPromptForMiddleware` and `applyTurnParamsRewrite` both read
|
|
5855
|
+
* this so they agree on one baseline: the first so a prompt-only caller
|
|
5856
|
+
* shows middleware its real text instead of an empty turn, the second so
|
|
5857
|
+
* a middleware-rewritten turn is compared against what middleware actually
|
|
5858
|
+
* started from — not a baseline that ignores `prompt` entirely, which
|
|
5859
|
+
* would make a prompt-only call look "unedited" no matter what middleware
|
|
5860
|
+
* did to it. `StreamOptions` carries no top-level `prompt` field, hence
|
|
5861
|
+
* the `"prompt" in options` guard instead of a cast.
|
|
5862
|
+
*/
|
|
5863
|
+
vertexTurnBaselineText(options) {
|
|
5864
|
+
if (options.input?.text !== undefined) {
|
|
5865
|
+
return options.input.text;
|
|
5866
|
+
}
|
|
5867
|
+
return "prompt" in options && typeof options.prompt === "string"
|
|
5868
|
+
? options.prompt
|
|
5869
|
+
: "";
|
|
5870
|
+
}
|
|
5871
|
+
/**
|
|
5872
|
+
* Build the V3 entry-turn prompt middleware sees for a native call.
|
|
5873
|
+
* Native Vertex loops build their own multi-turn conversation internally
|
|
5874
|
+
* once they run — this is only the caller's system prompt + text input,
|
|
5875
|
+
* the one turn middleware is given visibility into before that loop
|
|
5876
|
+
* starts.
|
|
5877
|
+
*
|
|
5878
|
+
* The user turn is an array of text parts rather than a bare string. That
|
|
5879
|
+
* is the V3 prompt shape, and prompt-editing middleware is written against
|
|
5880
|
+
* it: one that spreads `content` into a longer array turns a string into
|
|
5881
|
+
* one element per character, and one that calls `.map` / `.filter` on it
|
|
5882
|
+
* throws. The AI Studio path already hands its middleware parts
|
|
5883
|
+
* (`geminiContentsToV3Prompt`); this makes Vertex agree with it.
|
|
5884
|
+
*
|
|
5885
|
+
* The text itself comes from `vertexTurnBaselineText`, not
|
|
5886
|
+
* `options.input?.text` alone — a prompt-only `generateText()` caller
|
|
5887
|
+
* never sets `input`, and without the `options.prompt` fallback middleware
|
|
5888
|
+
* would be shown an empty turn and have nothing of the caller's to inspect
|
|
5889
|
+
* or build on.
|
|
5890
|
+
*/
|
|
5891
|
+
buildTurnPromptForMiddleware(options) {
|
|
5892
|
+
const prompt = [];
|
|
5893
|
+
if (options.systemPrompt) {
|
|
5894
|
+
prompt.push({ role: "system", content: options.systemPrompt });
|
|
5895
|
+
}
|
|
5896
|
+
prompt.push({
|
|
5897
|
+
role: "user",
|
|
5898
|
+
content: [{ type: "text", text: this.vertexTurnBaselineText(options) }],
|
|
5899
|
+
});
|
|
5900
|
+
return prompt;
|
|
5901
|
+
}
|
|
5902
|
+
/** Sampling params a native call already honors, carried onto the V3 view. */
|
|
5903
|
+
buildTurnV3Params(options, prompt) {
|
|
5904
|
+
return {
|
|
5905
|
+
prompt,
|
|
5906
|
+
...(options.maxTokens !== undefined
|
|
5907
|
+
? { maxOutputTokens: options.maxTokens }
|
|
5908
|
+
: {}),
|
|
5909
|
+
...(options.temperature !== undefined
|
|
5910
|
+
? { temperature: options.temperature }
|
|
5911
|
+
: {}),
|
|
5912
|
+
...(options.topP !== undefined ? { topP: options.topP } : {}),
|
|
5913
|
+
...(options.abortSignal !== undefined
|
|
5914
|
+
? { abortSignal: options.abortSignal }
|
|
5915
|
+
: {}),
|
|
5916
|
+
};
|
|
5917
|
+
}
|
|
5918
|
+
/**
|
|
5919
|
+
* Read a middleware-transformed prompt back into the two things a native
|
|
5920
|
+
* Vertex turn can carry: one system instruction and one user text.
|
|
5921
|
+
*
|
|
5922
|
+
* Every system message counts (joined with a blank line, the way
|
|
5923
|
+
* `v3PromptToGeminiContents` joins them for AI Studio) and so does every
|
|
5924
|
+
* user message, not just the last. A middleware that appends or prepends
|
|
5925
|
+
* context leaves the caller's own text in the prompt beside it; reading
|
|
5926
|
+
* only the last user message would send the addition without the question
|
|
5927
|
+
* it was meant to qualify, or drop it and send the question bare. Text
|
|
5928
|
+
* blocks — across messages and across the parts of one message — are joined
|
|
5929
|
+
* with a blank line for the same reason: they are separate blocks, and
|
|
5930
|
+
* gluing them runs the last word of one into the first of the next.
|
|
5931
|
+
*
|
|
5932
|
+
* `unmapped` counts what a native turn has no place for — non-text user
|
|
5933
|
+
* parts (files, images) and assistant or tool messages — so the caller can
|
|
5934
|
+
* say it was dropped rather than drop it silently.
|
|
5935
|
+
*/
|
|
5936
|
+
readTurnFromPrompt(prompt) {
|
|
5937
|
+
const systemTexts = prompt.flatMap((message) => message.role === "system" ? [message.content] : []);
|
|
5938
|
+
const userContents = prompt.flatMap((message) => message.role === "user" ? [message.content] : []);
|
|
5939
|
+
const userTexts = userContents
|
|
5940
|
+
.flatMap((content) => typeof content === "string"
|
|
5941
|
+
? [content]
|
|
5942
|
+
: content.flatMap((part) => part.type === "text" ? [part.text] : []))
|
|
5943
|
+
.filter((text) => text.length > 0);
|
|
5944
|
+
const unmappedParts = userContents.reduce((count, content) => typeof content === "string"
|
|
5945
|
+
? count
|
|
5946
|
+
: count + content.filter((part) => part.type !== "text").length, 0);
|
|
5947
|
+
const unmappedMessages = prompt.filter((message) => message.role !== "system" && message.role !== "user").length;
|
|
5948
|
+
return {
|
|
5949
|
+
systemPrompt: systemTexts.length > 0 ? systemTexts.join("\n\n") : undefined,
|
|
5950
|
+
text: userTexts.join("\n\n"),
|
|
5951
|
+
unmapped: unmappedParts + unmappedMessages,
|
|
5952
|
+
};
|
|
5953
|
+
}
|
|
5954
|
+
/**
|
|
5955
|
+
* Apply a middleware-transformed turn back onto the native call options.
|
|
5956
|
+
*
|
|
5957
|
+
* The system instruction and the user text are each written back only when
|
|
5958
|
+
* they differ from what middleware actually started from — `systemPrompt`
|
|
5959
|
+
* against `options.systemPrompt`, user text against
|
|
5960
|
+
* `vertexTurnBaselineText(options)`, not `options.input?.text` alone — so a
|
|
5961
|
+
* middleware that touches neither leaves both exactly as they were. An
|
|
5962
|
+
* edit is honoured in every direction — a changed, added or removed system
|
|
5963
|
+
* message, an appended or prepended user message, a user message emptied
|
|
5964
|
+
* outright (a redaction) — because every native generate/stream executor
|
|
5965
|
+
* resolves its wire text through `options.input.text` once this write-back
|
|
5966
|
+
* has run (`resolveNativeGenerateText` on the generate paths, which
|
|
5967
|
+
* special-cases exactly this divergence so an emptied `input.text` is not
|
|
5968
|
+
* mistaken for an unset one and does not fall back to the stale
|
|
5969
|
+
* `options.prompt` snapshot; a direct `options.input.text` read on the
|
|
5970
|
+
* stream paths, which have no such fallback to fall into). Comparing
|
|
5971
|
+
* against `options.input?.text` alone — ignoring `options.prompt` — would
|
|
5972
|
+
* make a prompt-only `generateText()` caller's untouched turn look
|
|
5973
|
+
* "changed" the first time middleware ran (nothing to compare `turn.text`
|
|
5974
|
+
* against but `""`), writing the real prompt into `input.text` as a
|
|
5975
|
+
* spurious edit on every such call even when no middleware did anything.
|
|
5976
|
+
*
|
|
5977
|
+
* Sampling params are written back the same way and can be removed as well
|
|
5978
|
+
* as changed: a middleware that drops `temperature` or `topP` from the
|
|
5979
|
+
* params must reach the wire as "not sent", not as the caller's original
|
|
5980
|
+
* value kept because the key went missing.
|
|
5981
|
+
*
|
|
5982
|
+
* `tools` is offered to middleware read-only — a middleware that rewrites
|
|
5983
|
+
* it gets a WARN rather than a silent drop, matching the OpenAI-compatible
|
|
5984
|
+
* streaming path's contract: native tool wiring is derived independently
|
|
5985
|
+
* of this V3 view, and re-deriving it here would diverge from it.
|
|
5986
|
+
*/
|
|
5987
|
+
applyTurnParamsRewrite(options, params) {
|
|
5988
|
+
if (params.tools !== undefined) {
|
|
5989
|
+
logger.warn("[GoogleVertex] middleware rewrote 'tools' on the native path; tool rewrites are not applied to the native request — the original tool list was sent.");
|
|
5990
|
+
}
|
|
5991
|
+
const turn = this.readTurnFromPrompt(params.prompt);
|
|
5992
|
+
if (turn.unmapped > 0) {
|
|
5993
|
+
logger.warn(`[GoogleVertex] middleware added ${turn.unmapped} prompt item(s) the native request cannot carry (non-text user parts, assistant or tool messages); only system and user text edits are applied — those items were not sent.`);
|
|
5994
|
+
}
|
|
5995
|
+
const maxTokens = typeof params.maxOutputTokens === "number"
|
|
5996
|
+
? params.maxOutputTokens
|
|
5997
|
+
: undefined;
|
|
5998
|
+
const temperature = typeof params.temperature === "number" ? params.temperature : undefined;
|
|
5999
|
+
const topP = typeof params.topP === "number" ? params.topP : undefined;
|
|
6000
|
+
return {
|
|
6001
|
+
...options,
|
|
6002
|
+
...(turn.systemPrompt !== (options.systemPrompt || undefined)
|
|
6003
|
+
? { systemPrompt: turn.systemPrompt }
|
|
6004
|
+
: {}),
|
|
6005
|
+
...(turn.text !== this.vertexTurnBaselineText(options)
|
|
6006
|
+
? { input: { ...(options.input ?? {}), text: turn.text } }
|
|
6007
|
+
: {}),
|
|
6008
|
+
...(maxTokens !== options.maxTokens ? { maxTokens } : {}),
|
|
6009
|
+
...(temperature !== options.temperature ? { temperature } : {}),
|
|
6010
|
+
...(topP !== options.topP ? { topP } : {}),
|
|
6011
|
+
};
|
|
6012
|
+
}
|
|
6013
|
+
/**
|
|
6014
|
+
* Strip the lifecycle middleware from the options handed to
|
|
6015
|
+
* `applyMiddlewareToModel` for a native Vertex call, when something else is
|
|
6016
|
+
* already firing the same callbacks.
|
|
6017
|
+
*
|
|
6018
|
+
* NeuroLink's `applyGenerateLifecycleMiddleware` /
|
|
6019
|
+
* `applyStreamLifecycleMiddleware` (neurolink.ts) mirror a caller's
|
|
6020
|
+
* top-level `onFinish` / `onError` / `onChunk` into
|
|
6021
|
+
* `options.middleware.middlewareConfig.lifecycle.config` — the exact same
|
|
6022
|
+
* callback references — before this provider sees `options`. Left enabled,
|
|
6023
|
+
* the built-in lifecycle middleware would fire them again from inside the
|
|
6024
|
+
* bridge, and with malformed payloads: it reads the legacy `textDelta` chunk
|
|
6025
|
+
* field and a bare `finishReason`, neither of which a V3 model produces.
|
|
6026
|
+
*
|
|
6027
|
+
* Who else fires them differs by path, so the exclusion does too:
|
|
6028
|
+
*
|
|
6029
|
+
* - generate: only `fireGenerateOnFinish` / `fireGenerateOnError`, which read
|
|
6030
|
+
* the TOP-LEVEL callbacks. The entry is stripped only when one is present.
|
|
6031
|
+
* A caller who configures `middlewareConfig.lifecycle` directly has no
|
|
6032
|
+
* other firer here, so that middleware applies exactly once, unmodified.
|
|
6033
|
+
* - stream: `BaseProvider.stream()` wraps every stream in
|
|
6034
|
+
* `wrapStreamWithLifecycleCallbacks`, which fires whatever sits in
|
|
6035
|
+
* `lifecycle.config` however it got there — top-level shorthand or direct
|
|
6036
|
+
* config. The entry is stripped whenever it exists, or a direct-config
|
|
6037
|
+
* caller would get every callback twice (and `onChunk` a third time, for
|
|
6038
|
+
* the `finish` part the middleware also reports).
|
|
6039
|
+
*
|
|
6040
|
+
* Only the `lifecycle` entry is disabled, so every other configured
|
|
6041
|
+
* middleware (guardrails, transformParams, custom hooks, analytics, ...)
|
|
6042
|
+
* applies normally.
|
|
6043
|
+
*/
|
|
6044
|
+
excludeAutoLifecycleMiddleware(options, path) {
|
|
6045
|
+
const opts = options;
|
|
6046
|
+
const firedElsewhere = path === "stream" || !!(opts.onFinish || opts.onError || opts.onChunk);
|
|
6047
|
+
if (!firedElsewhere) {
|
|
6048
|
+
return options;
|
|
6049
|
+
}
|
|
6050
|
+
const lifecycle = opts.middleware?.middlewareConfig?.lifecycle;
|
|
6051
|
+
if (!lifecycle) {
|
|
6052
|
+
return options;
|
|
6053
|
+
}
|
|
6054
|
+
return {
|
|
6055
|
+
...options,
|
|
6056
|
+
middleware: {
|
|
6057
|
+
...opts.middleware,
|
|
6058
|
+
middlewareConfig: {
|
|
6059
|
+
...opts.middleware?.middlewareConfig,
|
|
6060
|
+
lifecycle: { ...lifecycle, enabled: false },
|
|
6061
|
+
},
|
|
6062
|
+
},
|
|
6063
|
+
};
|
|
6064
|
+
}
|
|
6065
|
+
/**
|
|
6066
|
+
* Run the native Gemini generate loop and, when Vertex rejects an
|
|
6067
|
+
* over-constrained `responseSchema`, retry once without it.
|
|
6068
|
+
*
|
|
6069
|
+
* The retry sits inside the function `runNativeGenerateWithMiddleware` hands
|
|
6070
|
+
* to the model bridge rather than around the bridge, because it is
|
|
6071
|
+
* provider-internal recovery: model middleware must see one `doGenerate`
|
|
6072
|
+
* that succeeds. Retrying outside would show every middleware the failed
|
|
6073
|
+
* first call — the lifecycle middleware would report an `onError` for a
|
|
6074
|
+
* request that then succeeded, and `transformParams` would run twice.
|
|
6075
|
+
*/
|
|
6076
|
+
async executeNativeGemini3GenerateWithSchemaFallback(options, modelName) {
|
|
6077
|
+
try {
|
|
6078
|
+
return await this.executeNativeGemini3Generate(options);
|
|
6079
|
+
}
|
|
6080
|
+
catch (nativeError) {
|
|
6081
|
+
// Vertex rejects over-constrained responseSchemas with a
|
|
6082
|
+
// deterministic 400 ("too many states" — constrained-decoding
|
|
6083
|
+
// state explosion). Re-sending the same schema can never
|
|
6084
|
+
// succeed, so retry ONCE with the schema dropped but JSON
|
|
6085
|
+
// mode kept: output.format "json" still sets
|
|
6086
|
+
// responseMimeType application/json on the no-tools path,
|
|
6087
|
+
// so the model is forced to emit JSON — just without the
|
|
6088
|
+
// offending schema constraints. The NeuroLink layer then
|
|
6089
|
+
// coerces the JSON text into the caller's original schema
|
|
6090
|
+
// (generate({schema}) guarantee holds).
|
|
6091
|
+
const requestedStructured = options.schema !== undefined || options.output?.format === "json";
|
|
6092
|
+
// Tools present → the executor skips responseMimeType, so a
|
|
6093
|
+
// schema-less retry would NOT actually run in JSON mode and
|
|
6094
|
+
// the structured-output contract would silently degrade to
|
|
6095
|
+
// prose. Only the no-tools case retries faithfully; with
|
|
6096
|
+
// tools, surface the 400 to the caller instead.
|
|
6097
|
+
const hasTools = !options.disableTools && Object.keys(options.tools ?? {}).length > 0;
|
|
6098
|
+
if (requestedStructured &&
|
|
6099
|
+
!hasTools &&
|
|
6100
|
+
isSchemaComplexityError(nativeError)) {
|
|
6101
|
+
logger.warn("[GoogleVertex] responseSchema too complex for constrained decoding — retrying native generate in schema-less JSON mode", {
|
|
6102
|
+
model: modelName,
|
|
6103
|
+
error: nativeError instanceof Error
|
|
6104
|
+
? nativeError.message
|
|
6105
|
+
: String(nativeError),
|
|
6106
|
+
});
|
|
6107
|
+
return await this.executeNativeGemini3Generate({
|
|
6108
|
+
...options,
|
|
6109
|
+
schema: undefined,
|
|
6110
|
+
output: { format: "json" },
|
|
6111
|
+
});
|
|
6112
|
+
}
|
|
6113
|
+
throw nativeError;
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
6116
|
+
/**
|
|
6117
|
+
* Apply the configured middleware chain around a native Vertex generate
|
|
6118
|
+
* loop. `callNative` is `executeNativeAnthropicGenerate` or
|
|
6119
|
+
* `executeNativeGemini3Generate` — a self-contained black box that runs
|
|
6120
|
+
* its own multi-step tool loop and returns a fully-resolved
|
|
6121
|
+
* `EnhancedGenerateResult`. Wrapping happens around that call, not inside
|
|
6122
|
+
* it: `transformParams` can rewrite the entry turn before the loop starts,
|
|
6123
|
+
* and `wrapGenerate` can observe the result or replace it outright (a
|
|
6124
|
+
* guardrail that blocks the call entirely never reaches `callNative`, so
|
|
6125
|
+
* `nativeResult` stays unset and a settled result is synthesized purely
|
|
6126
|
+
* from what middleware produced, then run through `enhanceResult` like any
|
|
6127
|
+
* other result so `enableAnalytics` / `enableEvaluation` still apply).
|
|
6128
|
+
*/
|
|
6129
|
+
async runNativeGenerateWithMiddleware(options, modelName, callNative) {
|
|
6130
|
+
const startedAt = Date.now();
|
|
6131
|
+
const originalPrompt = this.buildTurnPromptForMiddleware(options);
|
|
6132
|
+
const providerName = this.providerName;
|
|
6133
|
+
let nativeResult;
|
|
6134
|
+
const baseModel = {
|
|
6135
|
+
specificationVersion: "v3",
|
|
6136
|
+
provider: providerName,
|
|
6137
|
+
modelId: modelName,
|
|
6138
|
+
supportedUrls: {},
|
|
6139
|
+
doGenerate: async (params) => {
|
|
6140
|
+
const effectiveOptions = this.applyTurnParamsRewrite(options, params);
|
|
6141
|
+
const result = await callNative(effectiveOptions);
|
|
6142
|
+
nativeResult = result;
|
|
6143
|
+
return {
|
|
6144
|
+
content: [{ type: "text", text: result.content }],
|
|
6145
|
+
finishReason: { unified: result.finishReason ?? "stop" },
|
|
6146
|
+
usage: {
|
|
6147
|
+
inputTokens: { total: result.usage?.input ?? 0 },
|
|
6148
|
+
outputTokens: { total: result.usage?.output ?? 0 },
|
|
6149
|
+
},
|
|
6150
|
+
};
|
|
6151
|
+
},
|
|
6152
|
+
doStream: () => {
|
|
6153
|
+
throw new Error("[GoogleVertex] doStream is not implemented on the native generate middleware model — this path only drives doGenerate.");
|
|
6154
|
+
},
|
|
6155
|
+
};
|
|
6156
|
+
const modelResult = await this.applyMiddlewareToModel(baseModel, this.excludeAutoLifecycleMiddleware(options, "generate"));
|
|
6157
|
+
if (typeof modelResult === "string") {
|
|
6158
|
+
throw new Error("[GoogleVertex] Native generate model handle required for middleware");
|
|
6159
|
+
}
|
|
6160
|
+
const wrappedModel = modelResult;
|
|
6161
|
+
const v3Params = this.buildTurnV3Params(options, originalPrompt);
|
|
6162
|
+
const v3Result = await wrappedModel.doGenerate(v3Params);
|
|
6163
|
+
const v3Text = v3Result.content
|
|
6164
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
6165
|
+
.join("");
|
|
6166
|
+
if (nativeResult) {
|
|
6167
|
+
// Reference/value equality against what the native call itself
|
|
6168
|
+
// returned is the "nothing changed" signal — return the native result
|
|
6169
|
+
// untouched (structuredData, toolCalls, analytics and all) rather than
|
|
6170
|
+
// a reconstruction that would drop fields this function doesn't know
|
|
6171
|
+
// about. Only a middleware that actually rewrote the text pays for a
|
|
6172
|
+
// merged object.
|
|
6173
|
+
return v3Text === nativeResult.content
|
|
6174
|
+
? nativeResult
|
|
6175
|
+
: { ...nativeResult, content: v3Text };
|
|
6176
|
+
}
|
|
6177
|
+
// Guardrails (or any middleware) blocked the call before `doGenerate`
|
|
6178
|
+
// ran — synthesize a settled result purely from the V3 response
|
|
6179
|
+
// middleware produced, the same "no native call, no content" contract
|
|
6180
|
+
// the OpenAI-compatible path's blocking case already proves out. The
|
|
6181
|
+
// turn bookkeeping a real loop would have filled in takes short-circuit
|
|
6182
|
+
// defaults, and `enhanceResult` runs on it as on every unblocked result:
|
|
6183
|
+
// without it a blocked call silently loses `analytics` / `evaluation`
|
|
6184
|
+
// that the same call would have carried had the guardrail let it through.
|
|
6185
|
+
return this.enhanceResult({
|
|
6186
|
+
content: v3Text,
|
|
6187
|
+
provider: providerName,
|
|
6188
|
+
model: modelName,
|
|
6189
|
+
finishReason: v3Result.finishReason.unified,
|
|
6190
|
+
usage: {
|
|
6191
|
+
input: v3Result.usage.inputTokens.total ?? 0,
|
|
6192
|
+
output: v3Result.usage.outputTokens.total ?? 0,
|
|
6193
|
+
total: (v3Result.usage.inputTokens.total ?? 0) +
|
|
6194
|
+
(v3Result.usage.outputTokens.total ?? 0),
|
|
6195
|
+
},
|
|
6196
|
+
toolCalls: [],
|
|
6197
|
+
stopReason: resolveTurnStopReason({
|
|
6198
|
+
timedOut: false,
|
|
6199
|
+
stalled: false,
|
|
6200
|
+
wasAborted: false,
|
|
6201
|
+
cappedWithoutAnswer: false,
|
|
6202
|
+
finishReason: v3Result.finishReason.unified,
|
|
6203
|
+
}),
|
|
6204
|
+
stepsUsed: 0,
|
|
6205
|
+
responseTime: Date.now() - startedAt,
|
|
6206
|
+
toolsUsed: [],
|
|
6207
|
+
toolExecutions: [],
|
|
6208
|
+
enhancedWithTools: false,
|
|
6209
|
+
}, options, startedAt);
|
|
6210
|
+
}
|
|
6211
|
+
/**
|
|
6212
|
+
* Apply the configured middleware chain around a native Vertex stream
|
|
6213
|
+
* loop. `callNative` is `executeNativeAnthropicStream` or
|
|
6214
|
+
* `executeNativeGemini3Stream`. Both are bridged through the same lazy V3
|
|
6215
|
+
* `ReadableStream` (`nativeChunksToV3Stream` / `v3StreamToNativeChunks`)
|
|
6216
|
+
* regardless of whether the underlying loop is genuinely live (Anthropic)
|
|
6217
|
+
* or already fully resolved (Gemini3) by the time it returns — pulling one
|
|
6218
|
+
* item at a time is correct for both, and never turns a live stream into
|
|
6219
|
+
* collect-then-replay.
|
|
6220
|
+
*
|
|
6221
|
+
* A guardrail that blocks the call never invokes `doStream`, so
|
|
6222
|
+
* `nativeResult` stays unset; a `StreamResult` is synthesized instead,
|
|
6223
|
+
* with `usage` / `finishReason` resolving once the merged stream is fully
|
|
6224
|
+
* drained (the same late-resolving-getter contract
|
|
6225
|
+
* `preserveStreamResultAccessors` already exists for).
|
|
6226
|
+
*/
|
|
6227
|
+
async runNativeStreamWithMiddleware(options, modelName, callNative) {
|
|
6228
|
+
const originalPrompt = this.buildTurnPromptForMiddleware(options);
|
|
6229
|
+
const providerName = this.providerName;
|
|
6230
|
+
let nativeResult;
|
|
6231
|
+
const baseModel = {
|
|
6232
|
+
specificationVersion: "v3",
|
|
6233
|
+
provider: providerName,
|
|
6234
|
+
modelId: modelName,
|
|
6235
|
+
supportedUrls: {},
|
|
6236
|
+
doGenerate: () => {
|
|
6237
|
+
throw new Error("[GoogleVertex] doGenerate is not implemented on the native stream middleware model — this path only drives doStream.");
|
|
6238
|
+
},
|
|
6239
|
+
doStream: async (params) => {
|
|
6240
|
+
const effectiveOptions = this.applyTurnParamsRewrite(options, params);
|
|
6241
|
+
const result = await callNative(effectiveOptions);
|
|
6242
|
+
nativeResult = result;
|
|
6243
|
+
return {
|
|
6244
|
+
stream: nativeChunksToV3Stream(result.stream, () => ({
|
|
6245
|
+
type: "finish",
|
|
6246
|
+
finishReason: { unified: result.finishReason ?? "stop" },
|
|
6247
|
+
usage: {
|
|
6248
|
+
inputTokens: { total: result.usage?.input ?? 0 },
|
|
6249
|
+
outputTokens: { total: result.usage?.output ?? 0 },
|
|
6250
|
+
},
|
|
6251
|
+
})),
|
|
6252
|
+
};
|
|
6253
|
+
},
|
|
6254
|
+
};
|
|
6255
|
+
const modelResult = await this.applyMiddlewareToModel(baseModel, this.excludeAutoLifecycleMiddleware(options, "stream"));
|
|
6256
|
+
if (typeof modelResult === "string") {
|
|
6257
|
+
throw new Error("[GoogleVertex] Native stream model handle required for middleware");
|
|
6258
|
+
}
|
|
6259
|
+
const wrappedModel = modelResult;
|
|
6260
|
+
const v3Params = this.buildTurnV3Params(options, originalPrompt);
|
|
6261
|
+
const { stream } = await wrappedModel.doStream(v3Params);
|
|
6262
|
+
let finishPart;
|
|
6263
|
+
const mergedStream = v3StreamToNativeChunks(stream, (part) => {
|
|
6264
|
+
finishPart = part;
|
|
6265
|
+
});
|
|
6266
|
+
if (nativeResult) {
|
|
6267
|
+
const settled = nativeResult;
|
|
6268
|
+
return this.preserveStreamResultAccessors(settled, {
|
|
6269
|
+
...settled,
|
|
6270
|
+
stream: mergedStream,
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
// Guardrails (or any middleware) blocked the call before `doStream`
|
|
6274
|
+
// ran — synthesize a settled StreamResult purely from the V3 stream
|
|
6275
|
+
// middleware produced. `usage` / `finishReason` are getters so a
|
|
6276
|
+
// consumer who reads them before draining `stream` doesn't just see a
|
|
6277
|
+
// permanent snapshot of "nothing yet" — same contract the native
|
|
6278
|
+
// Anthropic path's own getters already rely on.
|
|
6279
|
+
const blockedResult = {
|
|
6280
|
+
stream: mergedStream,
|
|
6281
|
+
provider: providerName,
|
|
6282
|
+
model: modelName,
|
|
6283
|
+
};
|
|
6284
|
+
Object.defineProperty(blockedResult, "usage", {
|
|
6285
|
+
enumerable: true,
|
|
6286
|
+
configurable: true,
|
|
6287
|
+
get: () => finishPart
|
|
6288
|
+
? {
|
|
6289
|
+
input: finishPart.usage.inputTokens.total ?? 0,
|
|
6290
|
+
output: finishPart.usage.outputTokens.total ?? 0,
|
|
6291
|
+
total: (finishPart.usage.inputTokens.total ?? 0) +
|
|
6292
|
+
(finishPart.usage.outputTokens.total ?? 0),
|
|
6293
|
+
}
|
|
6294
|
+
: undefined,
|
|
6295
|
+
});
|
|
6296
|
+
Object.defineProperty(blockedResult, "finishReason", {
|
|
6297
|
+
enumerable: true,
|
|
6298
|
+
configurable: true,
|
|
6299
|
+
get: () => finishPart?.finishReason.unified,
|
|
6300
|
+
});
|
|
6301
|
+
return blockedResult;
|
|
6302
|
+
}
|
|
5734
6303
|
/**
|
|
5735
6304
|
* Re-apply getter-based accessor properties from a source StreamResult onto
|
|
5736
6305
|
* a wrapper copy. Wrapper spreads (`{ ...result }`) invoke and SNAPSHOT
|
|
5737
6306
|
* enumerable getters at wrap time — for background-loop streams (the native
|
|
5738
6307
|
* Anthropic path) that resolve finishReason / structuredOutput / toolCalls
|
|
5739
|
-
* only as the consumer drains, the snapshot is permanently
|
|
5740
|
-
* Copying the accessor descriptors keeps the wrapped
|
|
5741
|
-
* built from plain data properties (the buffered
|
|
5742
|
-
*
|
|
6308
|
+
* / usage only as the consumer drains, the snapshot is permanently
|
|
6309
|
+
* undefined/empty. Copying the accessor descriptors keeps the wrapped
|
|
6310
|
+
* result live. Results built from plain data properties (the buffered
|
|
6311
|
+
* Gemini paths, and the native Anthropic stream's own `usage` — a mutable
|
|
6312
|
+
* object reference rather than a getter, so a spread keeps its identity
|
|
6313
|
+
* and later mutations stay visible) have no matching getter here and pass
|
|
6314
|
+
* through untouched.
|
|
6315
|
+
*
|
|
6316
|
+
* The re-applied property also accepts a direct write: NeuroLink.stream()
|
|
6317
|
+
* backstops a missing `usage` from analytics once the outer stream drains
|
|
6318
|
+
* by assigning `mcpStreamOutcome.usage = resolvedUsage` on whatever result
|
|
6319
|
+
* object it was handed. Copying the bare getter-only descriptor verbatim
|
|
6320
|
+
* would turn that assignment into a TypeError ("Cannot set property usage
|
|
6321
|
+
* of #<Object> which has only a getter"), so each re-applied accessor is
|
|
6322
|
+
* rebuilt with a setter that remembers an override and prefers it over the
|
|
6323
|
+
* live source getter once one has been written.
|
|
5743
6324
|
*/
|
|
5744
6325
|
preserveStreamResultAccessors(source, wrapped) {
|
|
5745
6326
|
for (const key of [
|
|
@@ -5748,11 +6329,24 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5748
6329
|
"toolCalls",
|
|
5749
6330
|
"toolsUsed",
|
|
5750
6331
|
"toolExecutions",
|
|
6332
|
+
"usage",
|
|
5751
6333
|
]) {
|
|
5752
6334
|
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
5753
|
-
|
|
5754
|
-
|
|
6335
|
+
const liveGetter = descriptor?.get;
|
|
6336
|
+
if (!liveGetter) {
|
|
6337
|
+
continue;
|
|
5755
6338
|
}
|
|
6339
|
+
let overridden = false;
|
|
6340
|
+
let overrideValue;
|
|
6341
|
+
Object.defineProperty(wrapped, key, {
|
|
6342
|
+
configurable: true,
|
|
6343
|
+
enumerable: descriptor.enumerable ?? true,
|
|
6344
|
+
get: () => (overridden ? overrideValue : liveGetter.call(source)),
|
|
6345
|
+
set: (value) => {
|
|
6346
|
+
overridden = true;
|
|
6347
|
+
overrideValue = value;
|
|
6348
|
+
},
|
|
6349
|
+
});
|
|
5756
6350
|
}
|
|
5757
6351
|
return wrapped;
|
|
5758
6352
|
}
|