@juspay/neurolink 12.41.3 → 12.41.5
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 +410 -408
- package/dist/core/baseProvider.js +24 -13
- package/dist/middleware/builtin/lifecycle.js +29 -6
- package/dist/neurolink.js +16 -13
- package/dist/providers/amazonBedrock/client.d.ts +27 -0
- package/dist/providers/amazonBedrock/client.js +418 -113
- package/dist/providers/amazonBedrock/modelBridge.d.ts +100 -0
- package/dist/providers/amazonBedrock/modelBridge.js +226 -0
- package/dist/providers/googleVertex/client.d.ts +20 -0
- package/dist/providers/googleVertex/client.js +42 -20
- package/dist/utils/lifecycleCallbacks.d.ts +30 -1
- package/dist/utils/lifecycleCallbacks.js +93 -0
- package/package.json +1 -1
|
@@ -6,7 +6,9 @@ import { DEFAULT_MAX_STEPS } from "../../core/constants.js";
|
|
|
6
6
|
import { runAgenticLoop } from "../../core/loopEngine.js";
|
|
7
7
|
import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
|
|
8
8
|
import { transformToolExecutions } from "../../utils/transformationUtils.js";
|
|
9
|
+
import { composeAbortSignals } from "../../utils/timeout.js";
|
|
9
10
|
import { createBedrockLoopAdapter } from "./loopAdapter.js";
|
|
11
|
+
import { bedrockChunksToV3Stream, bedrockConversationFromV3Prompt, bedrockV3StreamToChunks, buildV3PromptFromBedrock, isTextOnlyBedrockConversation, unsentBedrockMiddlewareOptions, unwrapBedrockToolInputSchema, } from "./modelBridge.js";
|
|
10
12
|
import { AuthenticationError, ProviderError, RateLimitError, } from "../../types/index.js";
|
|
11
13
|
import { classifyProviderError } from "../../utils/errorClassifier.js";
|
|
12
14
|
import { isAbortError, withTimeout } from "../../utils/errorHandling.js";
|
|
@@ -351,47 +353,173 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
351
353
|
const tools = await this.resolveTurnTools(options.tools, options.disableTools);
|
|
352
354
|
const toolConfig = this.formatToolsForBedrock(tools);
|
|
353
355
|
const sampling = resolveSamplingParams(this.providerName, this.modelName || this.getDefaultModel(), { temperature: options.temperature ?? 0.7 }, "bedrock.converse");
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
356
|
+
const systemPromptText = options.systemPrompt ||
|
|
357
|
+
"You are a helpful assistant with access to external tools. Use tools when necessary to provide accurate information.";
|
|
358
|
+
// Builds a fresh adapter per turn — the generate model's `doGenerate` may
|
|
359
|
+
// run this more than once is never true today (one call per turn), but a
|
|
360
|
+
// fresh adapter keeps `buildCommandInput` closed over the exact
|
|
361
|
+
// (possibly middleware-transformed) system/conversation/sampling for
|
|
362
|
+
// that call, rather than the outer ones.
|
|
363
|
+
const runTurn = (conversation, system, maxTokens, temperature, signal) => {
|
|
364
|
+
const adapter = createBedrockLoopAdapter({
|
|
365
|
+
client: this.bedrockClient,
|
|
366
|
+
streaming: false,
|
|
367
|
+
region: this.region,
|
|
368
|
+
maxSteps,
|
|
369
|
+
buildCommandInput: (conv) => ({
|
|
370
|
+
modelId: this.modelName || this.getDefaultModel(),
|
|
371
|
+
messages: this.convertToAWSMessages(conv),
|
|
372
|
+
// Converse rejects a blank block; transformParams may empty this.
|
|
373
|
+
...(system ? { system: [{ text: system }] } : {}),
|
|
374
|
+
inferenceConfig: {
|
|
375
|
+
maxTokens,
|
|
376
|
+
...(temperature !== undefined && { temperature }),
|
|
366
377
|
},
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
temperature: sampling.temperature,
|
|
372
|
-
}),
|
|
373
|
-
},
|
|
374
|
-
...(toolConfig ? { toolConfig } : {}),
|
|
375
|
-
}),
|
|
376
|
-
});
|
|
377
|
-
try {
|
|
378
|
-
const { resultPromise } = runAgenticLoop(adapter, this.conversationHistory, {
|
|
378
|
+
...(toolConfig ? { toolConfig } : {}),
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
const { resultPromise } = runAgenticLoop(adapter, conversation, {
|
|
379
382
|
tools: this.toEngineTools(tools),
|
|
380
|
-
abortSignal:
|
|
383
|
+
abortSignal: signal,
|
|
381
384
|
// `toEngineTools` adds no deadline of its own, so this is the only
|
|
382
385
|
// per-tool bound on the turn. The engine defaults when it is absent.
|
|
383
386
|
...(options.toolTimeoutMs !== undefined
|
|
384
387
|
? { toolTimeoutMs: options.toolTimeoutMs }
|
|
385
388
|
: {}),
|
|
386
389
|
});
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
390
|
+
return resultPromise;
|
|
391
|
+
};
|
|
392
|
+
try {
|
|
393
|
+
const initialConversation = this.conversationHistory;
|
|
394
|
+
let text;
|
|
395
|
+
let finishReason;
|
|
396
|
+
let rawFinishReason;
|
|
397
|
+
let usage;
|
|
398
|
+
let toolExecutions;
|
|
399
|
+
let conversationOut;
|
|
400
|
+
if (isTextOnlyBedrockConversation(initialConversation)) {
|
|
401
|
+
// Model middleware (transformParams/wrapGenerate) applies only to a
|
|
402
|
+
// text-only starting turn — see modelBridge.ts's header comment.
|
|
403
|
+
const v3Tools = toolConfig?.tools
|
|
404
|
+
?.map((t) => t.toolSpec)
|
|
405
|
+
.filter((spec) => !!spec)
|
|
406
|
+
.map((spec) => ({
|
|
407
|
+
type: "function",
|
|
408
|
+
name: spec.name ?? "",
|
|
409
|
+
description: spec.description,
|
|
410
|
+
inputSchema: unwrapBedrockToolInputSchema(spec.inputSchema),
|
|
411
|
+
}));
|
|
412
|
+
// Set only if `doGenerate` actually ran the loop — a guardrails-style
|
|
413
|
+
// wrapGenerate can answer from its own precall without ever calling
|
|
414
|
+
// it, and the richer loop result (toolExecutions/conversation) is
|
|
415
|
+
// then unavailable, tolerated below via `loopResult?.`.
|
|
416
|
+
let capturedLoopResult;
|
|
417
|
+
const generateModel = {
|
|
418
|
+
specificationVersion: "v3",
|
|
419
|
+
provider: this.providerName,
|
|
420
|
+
modelId: this.modelName || this.getDefaultModel(),
|
|
421
|
+
supportedUrls: {},
|
|
422
|
+
doGenerate: async (params) => {
|
|
423
|
+
if (params.tools !== undefined && params.tools !== v3Tools) {
|
|
424
|
+
logger.warn("[AmazonBedrockProvider] middleware rewrote 'tools' on the generate path; the rewrite was ignored and the caller's original tool configuration was sent");
|
|
425
|
+
}
|
|
426
|
+
this.warnOnUnsentMiddlewareOptions("generate", params);
|
|
427
|
+
// Read AFTER transformParams: by AI SDK contract, `params` here
|
|
428
|
+
// is already the transformed version, so a prompt rewrite
|
|
429
|
+
// reaches the wire.
|
|
430
|
+
const { system, conversation } = bedrockConversationFromV3Prompt(params.prompt);
|
|
431
|
+
const loopResult = runTurn(conversation, system, params.maxOutputTokens ?? options.maxTokens, params.temperature ?? sampling.temperature,
|
|
432
|
+
// Composed, not `??`: a middleware's signal must not displace
|
|
433
|
+
// the caller's.
|
|
434
|
+
composeAbortSignals(options.abortSignal, params.abortSignal));
|
|
435
|
+
capturedLoopResult = loopResult;
|
|
436
|
+
const resolved = await loopResult;
|
|
437
|
+
return {
|
|
438
|
+
content: resolved.text
|
|
439
|
+
? [{ type: "text", text: resolved.text }]
|
|
440
|
+
: [],
|
|
441
|
+
finishReason: {
|
|
442
|
+
unified: resolved.finishReason,
|
|
443
|
+
raw: resolved.rawStopReason,
|
|
444
|
+
},
|
|
445
|
+
usage: {
|
|
446
|
+
inputTokens: {
|
|
447
|
+
// `inputTokens` is the fresh portion only; AWS reports cache
|
|
448
|
+
// reads/writes beside it, so `total` sums all three.
|
|
449
|
+
total: resolved.usage.inputTokens +
|
|
450
|
+
(resolved.usage.cacheReadTokens ?? 0) +
|
|
451
|
+
(resolved.usage.cacheWriteTokens ?? 0),
|
|
452
|
+
noCache: resolved.usage.inputTokens,
|
|
453
|
+
cacheRead: resolved.usage.cacheReadTokens,
|
|
454
|
+
cacheWrite: resolved.usage.cacheWriteTokens,
|
|
455
|
+
},
|
|
456
|
+
outputTokens: { total: resolved.usage.outputTokens },
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
},
|
|
460
|
+
// This model exists only to be driven through doGenerate.
|
|
461
|
+
doStream: () => {
|
|
462
|
+
throw new Error("AmazonBedrockProvider's generate model does not implement doStream");
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
const wrappedModel = await this.applyMiddlewareToModel(generateModel, options);
|
|
466
|
+
if (typeof wrappedModel === "string") {
|
|
467
|
+
throw new Error("AmazonBedrockProvider: middleware returned a model id string, not a native model handle");
|
|
468
|
+
}
|
|
469
|
+
const v3Result = await wrappedModel.doGenerate({
|
|
470
|
+
prompt: buildV3PromptFromBedrock(systemPromptText, initialConversation),
|
|
471
|
+
maxOutputTokens: options.maxTokens,
|
|
472
|
+
temperature: sampling.temperature,
|
|
473
|
+
...(v3Tools ? { tools: v3Tools } : {}),
|
|
474
|
+
// Seed the signal so a middleware can compose with it, not replace it.
|
|
475
|
+
abortSignal: options.abortSignal,
|
|
476
|
+
});
|
|
477
|
+
// Settled, not re-thrown: a wrapGenerate may have caught the loop's
|
|
478
|
+
// rejection and answered itself, and a bare `await` would re-throw it
|
|
479
|
+
// over that recovered result.
|
|
480
|
+
const loopResult = await capturedLoopResult?.catch(() => undefined);
|
|
481
|
+
text = v3Result.content
|
|
482
|
+
.filter((part) => part.type === "text")
|
|
483
|
+
.map((part) => part.text)
|
|
484
|
+
.join("");
|
|
485
|
+
finishReason =
|
|
486
|
+
loopResult?.finishReason ?? v3Result.finishReason.unified;
|
|
487
|
+
rawFinishReason =
|
|
488
|
+
loopResult?.rawStopReason ?? v3Result.finishReason.raw;
|
|
489
|
+
usage = loopResult?.usage ?? {
|
|
490
|
+
// `AgenticLoopUsage.inputTokens` is the fresh count, so read
|
|
491
|
+
// `noCache`; `total` would double-count the cache tokens below.
|
|
492
|
+
inputTokens: v3Result.usage.inputTokens.noCache ??
|
|
493
|
+
v3Result.usage.inputTokens.total ??
|
|
494
|
+
0,
|
|
495
|
+
outputTokens: v3Result.usage.outputTokens.total ?? 0,
|
|
496
|
+
...(v3Result.usage.inputTokens.cacheRead !== undefined && {
|
|
497
|
+
cacheReadTokens: v3Result.usage.inputTokens.cacheRead,
|
|
498
|
+
}),
|
|
499
|
+
...(v3Result.usage.inputTokens.cacheWrite !== undefined && {
|
|
500
|
+
cacheWriteTokens: v3Result.usage.inputTokens.cacheWrite,
|
|
501
|
+
}),
|
|
502
|
+
};
|
|
503
|
+
toolExecutions = loopResult?.toolExecutions ?? [];
|
|
504
|
+
conversationOut = loopResult?.conversation ?? initialConversation;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
this.logMultimodalMiddlewareSkip(options);
|
|
508
|
+
const loopResult = await runTurn(initialConversation, systemPromptText, options.maxTokens, sampling.temperature, options.abortSignal);
|
|
509
|
+
text = loopResult.text || "";
|
|
510
|
+
finishReason = loopResult.finishReason;
|
|
511
|
+
rawFinishReason = loopResult.rawStopReason;
|
|
512
|
+
usage = loopResult.usage;
|
|
513
|
+
toolExecutions = loopResult.toolExecutions;
|
|
514
|
+
conversationOut = loopResult.conversation;
|
|
515
|
+
}
|
|
516
|
+
this.conversationHistory = conversationOut;
|
|
517
|
+
const input = usage.inputTokens;
|
|
518
|
+
const output = usage.outputTokens;
|
|
519
|
+
const cacheRead = usage.cacheReadTokens ?? 0;
|
|
520
|
+
const cacheWrite = usage.cacheWriteTokens ?? 0;
|
|
393
521
|
return {
|
|
394
|
-
text:
|
|
522
|
+
text: text || "",
|
|
395
523
|
usage: {
|
|
396
524
|
input,
|
|
397
525
|
output,
|
|
@@ -406,9 +534,9 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
406
534
|
// so a turn stopped at the step cap reported "tool_use" here while
|
|
407
535
|
// the same turn reported "tool-calls" when streamed. The raw value is
|
|
408
536
|
// kept alongside rather than dropped.
|
|
409
|
-
finishReason
|
|
410
|
-
rawFinishReason
|
|
411
|
-
toolExecutions
|
|
537
|
+
finishReason,
|
|
538
|
+
rawFinishReason,
|
|
539
|
+
toolExecutions,
|
|
412
540
|
};
|
|
413
541
|
}
|
|
414
542
|
catch (error) {
|
|
@@ -822,12 +950,24 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
822
950
|
catch (error) {
|
|
823
951
|
logger.debug("[TRACE] executeStream CATCH - error caught from streamingConversationLoop");
|
|
824
952
|
const errorObj = error;
|
|
825
|
-
// Check if error is related to streaming permissions
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
953
|
+
// Check if error is related to streaming permissions. Gated on the
|
|
954
|
+
// real AWS SDK service-exception shape ($metadata) first: a
|
|
955
|
+
// middleware's wrapStream/transformParams can throw an arbitrary
|
|
956
|
+
// Error whose message happens to contain "streaming" or whose name
|
|
957
|
+
// happens to collide with an AWS exception name, and without this
|
|
958
|
+
// guard that error was silently swallowed and retried as if AWS
|
|
959
|
+
// itself had rejected the streaming call (see
|
|
960
|
+
// BedrockRuntimeServiceException in the AWS SDK, which carries
|
|
961
|
+
// $fault/$metadata on every genuine service exception).
|
|
962
|
+
const hasAwsServiceExceptionShape = typeof errorObj === "object" &&
|
|
963
|
+
errorObj !== null &&
|
|
964
|
+
"$metadata" in errorObj;
|
|
965
|
+
const isPermissionError = hasAwsServiceExceptionShape &&
|
|
966
|
+
(errorObj?.name === "AccessDeniedException" ||
|
|
967
|
+
errorObj?.name === "UnauthorizedOperation" ||
|
|
968
|
+
errorObj?.message?.includes("bedrock:InvokeModelWithResponseStream") ||
|
|
969
|
+
errorObj?.message?.includes("streaming") ||
|
|
970
|
+
errorObj?.message?.includes("ConverseStream"));
|
|
831
971
|
logger.debug("[TRACE] executeStream CATCH - checking if permission error");
|
|
832
972
|
logger.debug(`[TRACE] executeStream CATCH - isPermissionError=${isPermissionError}`);
|
|
833
973
|
if (isPermissionError) {
|
|
@@ -922,6 +1062,152 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
922
1062
|
}
|
|
923
1063
|
});
|
|
924
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* A multimodal turn skips model middleware (see modelBridge.ts's header).
|
|
1067
|
+
* WARN when middleware was configured, per request or on the provider,
|
|
1068
|
+
* since it silently did not run; multimodal turns alone are unremarkable,
|
|
1069
|
+
* so stay at debug otherwise.
|
|
1070
|
+
*/
|
|
1071
|
+
logMultimodalMiddlewareSkip(options) {
|
|
1072
|
+
if (options.middleware ?? this.middlewareOptions) {
|
|
1073
|
+
logger.warn("[AmazonBedrockProvider] multimodal turn — configured middleware was NOT applied (text-only turns only)");
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
logger.debug("[AmazonBedrockProvider] multimodal turn — skipping model middleware wrapping (text-only turns only)");
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* `inferenceConfig` carries `maxTokens` and `temperature` only, so any other
|
|
1080
|
+
* sampling option a middleware sets would be dropped without a trace.
|
|
1081
|
+
* `tools` is reported where it is compared, at each call site.
|
|
1082
|
+
*/
|
|
1083
|
+
warnOnUnsentMiddlewareOptions(path, params) {
|
|
1084
|
+
const unsent = unsentBedrockMiddlewareOptions(params);
|
|
1085
|
+
if (unsent.length > 0) {
|
|
1086
|
+
logger.warn(`[AmazonBedrockProvider] middleware set options the native Bedrock path does not send on the ${path} path; ignored: ${unsent.map((name) => `'${name}'`).join(", ")}`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Runs the turn behind `streamingConversationLoop`: the model-middleware
|
|
1091
|
+
* path for a text-only starting turn, or the direct fallback for a
|
|
1092
|
+
* multimodal one. Extracted only to keep `streamingConversationLoop`
|
|
1093
|
+
* readable — the branching logic itself is unchanged from what it
|
|
1094
|
+
* replaced.
|
|
1095
|
+
*/
|
|
1096
|
+
async buildBedrockStreamTurn(params) {
|
|
1097
|
+
const { initialConversation, systemPromptText, options, sampling, toolConfig, turnAbort, runTurn, raceFirstStep, } = params;
|
|
1098
|
+
if (!isTextOnlyBedrockConversation(initialConversation)) {
|
|
1099
|
+
this.logMultimodalMiddlewareSkip(options);
|
|
1100
|
+
const turn = runTurn(initialConversation, systemPromptText, options.maxTokens, sampling.temperature, turnAbort.signal);
|
|
1101
|
+
await raceFirstStep(turn.resultPromise);
|
|
1102
|
+
return { stream: turn.stream, capturedLoopResult: turn.resultPromise };
|
|
1103
|
+
}
|
|
1104
|
+
// Model middleware (transformParams/wrapStream) applies only to a
|
|
1105
|
+
// text-only starting turn — see modelBridge.ts's header comment.
|
|
1106
|
+
const v3Tools = toolConfig?.tools
|
|
1107
|
+
?.map((t) => t.toolSpec)
|
|
1108
|
+
.filter((spec) => !!spec)
|
|
1109
|
+
.map((spec) => ({
|
|
1110
|
+
type: "function",
|
|
1111
|
+
name: spec.name ?? "",
|
|
1112
|
+
description: spec.description,
|
|
1113
|
+
inputSchema: unwrapBedrockToolInputSchema(spec.inputSchema),
|
|
1114
|
+
}));
|
|
1115
|
+
// Set only once the loop actually starts — a guardrails-style wrapStream
|
|
1116
|
+
// can answer from its own precall without ever calling `doStream`
|
|
1117
|
+
// (correction 3), and the richer loop result (toolExecutions/
|
|
1118
|
+
// conversation/mapped finishReason) is then unavailable. The caller
|
|
1119
|
+
// tolerates `undefined`.
|
|
1120
|
+
let capturedLoopResult;
|
|
1121
|
+
const streamModel = {
|
|
1122
|
+
specificationVersion: "v3",
|
|
1123
|
+
provider: this.providerName,
|
|
1124
|
+
modelId: this.modelName || this.getDefaultModel(),
|
|
1125
|
+
supportedUrls: {},
|
|
1126
|
+
// This model exists only to be driven through doStream.
|
|
1127
|
+
doGenerate: () => {
|
|
1128
|
+
throw new Error("AmazonBedrockProvider's stream model does not implement doGenerate");
|
|
1129
|
+
},
|
|
1130
|
+
doStream: async (params) => {
|
|
1131
|
+
if (params.tools !== undefined && params.tools !== v3Tools) {
|
|
1132
|
+
logger.warn("[AmazonBedrockProvider] middleware rewrote 'tools' on the stream path; the rewrite was ignored and the caller's original tool configuration was sent");
|
|
1133
|
+
}
|
|
1134
|
+
this.warnOnUnsentMiddlewareOptions("stream", params);
|
|
1135
|
+
// Read AFTER transformParams: by AI SDK contract, `params` here is
|
|
1136
|
+
// already the transformed version, so a prompt rewrite reaches the
|
|
1137
|
+
// wire (correction 1).
|
|
1138
|
+
const { system, conversation } = bedrockConversationFromV3Prompt(params.prompt);
|
|
1139
|
+
const turn = runTurn(conversation, system, params.maxOutputTokens ?? options.maxTokens, params.temperature ?? sampling.temperature,
|
|
1140
|
+
// Composed, not `??`: turnAbort.signal already carries the
|
|
1141
|
+
// caller's abort, BaseProvider's teardown signal, and the stream
|
|
1142
|
+
// consumer's own cancel() (wired below) — a middleware's
|
|
1143
|
+
// transformParams-set abortSignal must add to that, never replace
|
|
1144
|
+
// it outright.
|
|
1145
|
+
composeAbortSignals(turnAbort.signal, params.abortSignal));
|
|
1146
|
+
capturedLoopResult = turn.resultPromise;
|
|
1147
|
+
await raceFirstStep(turn.resultPromise);
|
|
1148
|
+
const finish = turn.resultPromise.then((result) => ({
|
|
1149
|
+
type: "finish",
|
|
1150
|
+
finishReason: {
|
|
1151
|
+
unified: result.finishReason,
|
|
1152
|
+
raw: result.rawStopReason,
|
|
1153
|
+
},
|
|
1154
|
+
usage: {
|
|
1155
|
+
inputTokens: {
|
|
1156
|
+
// Same fresh-vs-total split as the generate path above —
|
|
1157
|
+
// `result.usage.inputTokens` excludes cache reads/writes.
|
|
1158
|
+
total: result.usage.inputTokens +
|
|
1159
|
+
(result.usage.cacheReadTokens ?? 0) +
|
|
1160
|
+
(result.usage.cacheWriteTokens ?? 0),
|
|
1161
|
+
noCache: result.usage.inputTokens,
|
|
1162
|
+
cacheRead: result.usage.cacheReadTokens,
|
|
1163
|
+
cacheWrite: result.usage.cacheWriteTokens,
|
|
1164
|
+
},
|
|
1165
|
+
outputTokens: { total: result.usage.outputTokens },
|
|
1166
|
+
},
|
|
1167
|
+
}));
|
|
1168
|
+
// The turn can reject before anything reads this terminal event.
|
|
1169
|
+
void finish.catch(() => undefined);
|
|
1170
|
+
return {
|
|
1171
|
+
stream: bedrockChunksToV3Stream(turn.stream, finish, () => turnAbort.abort()),
|
|
1172
|
+
};
|
|
1173
|
+
},
|
|
1174
|
+
};
|
|
1175
|
+
const wrappedModel = await this.applyMiddlewareToModel(streamModel, options);
|
|
1176
|
+
if (typeof wrappedModel === "string") {
|
|
1177
|
+
throw new Error("AmazonBedrockProvider: middleware returned a model id string, not a native model handle");
|
|
1178
|
+
}
|
|
1179
|
+
const v3StreamResult = await wrappedModel.doStream({
|
|
1180
|
+
prompt: buildV3PromptFromBedrock(systemPromptText, initialConversation),
|
|
1181
|
+
maxOutputTokens: options.maxTokens,
|
|
1182
|
+
temperature: sampling.temperature,
|
|
1183
|
+
...(v3Tools ? { tools: v3Tools } : {}),
|
|
1184
|
+
abortSignal: turnAbort.signal,
|
|
1185
|
+
});
|
|
1186
|
+
return {
|
|
1187
|
+
stream: bedrockV3StreamToChunks(v3StreamResult.stream),
|
|
1188
|
+
capturedLoopResult,
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Turns a settled loop outcome's usage into the shape both the analytics
|
|
1193
|
+
* resolution and the stream's own `finally` block report. Shared so the
|
|
1194
|
+
* two call sites in `streamingConversationLoop` can't drift.
|
|
1195
|
+
*/
|
|
1196
|
+
summarizeBedrockStreamUsage(usage) {
|
|
1197
|
+
const input = usage?.inputTokens ?? 0;
|
|
1198
|
+
const output = usage?.outputTokens ?? 0;
|
|
1199
|
+
const cacheRead = usage?.cacheReadTokens ?? 0;
|
|
1200
|
+
const cacheWrite = usage?.cacheWriteTokens ?? 0;
|
|
1201
|
+
return {
|
|
1202
|
+
input,
|
|
1203
|
+
output,
|
|
1204
|
+
// Cache reads/writes are billed tokens reported separately from
|
|
1205
|
+
// inputTokens — the total must include them.
|
|
1206
|
+
total: input + cacheRead + cacheWrite + output,
|
|
1207
|
+
...(cacheRead > 0 && { cacheReadTokens: cacheRead }),
|
|
1208
|
+
...(cacheWrite > 0 && { cacheCreationTokens: cacheWrite }),
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
925
1211
|
async streamingConversationLoop(options, streamSpan) {
|
|
926
1212
|
const startTime = Date.now();
|
|
927
1213
|
const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
|
|
@@ -930,29 +1216,8 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
930
1216
|
const tools = await this.resolveTurnTools(options.tools, options.disableTools);
|
|
931
1217
|
const toolConfig = this.formatToolsForBedrock(tools);
|
|
932
1218
|
const sampling = resolveSamplingParams(this.providerName, this.modelName || this.getDefaultModel(), { temperature: options.temperature ?? 0.7 }, "bedrock.converseStream");
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
streaming: true,
|
|
936
|
-
region: this.region,
|
|
937
|
-
maxSteps,
|
|
938
|
-
buildCommandInput: (conversation) => ({
|
|
939
|
-
modelId: this.modelName || this.getDefaultModel(),
|
|
940
|
-
messages: this.convertToAWSMessages(conversation),
|
|
941
|
-
system: [
|
|
942
|
-
{
|
|
943
|
-
text: options.systemPrompt ||
|
|
944
|
-
"You are a helpful assistant with access to external tools. Use tools when necessary to provide accurate information.",
|
|
945
|
-
},
|
|
946
|
-
],
|
|
947
|
-
inferenceConfig: {
|
|
948
|
-
maxTokens: options.maxTokens,
|
|
949
|
-
...(sampling.temperature !== undefined && {
|
|
950
|
-
temperature: sampling.temperature,
|
|
951
|
-
}),
|
|
952
|
-
},
|
|
953
|
-
...(toolConfig ? { toolConfig } : {}),
|
|
954
|
-
}),
|
|
955
|
-
});
|
|
1219
|
+
const systemPromptText = options.systemPrompt ||
|
|
1220
|
+
"You are a helpful assistant with access to external tools. Use tools when necessary to provide accurate information.";
|
|
956
1221
|
// executeStream() falls back to non-streaming generate() when the first
|
|
957
1222
|
// call fails on permissions, and that only works if the failure reaches it
|
|
958
1223
|
// synchronously. The engine runs the turn in the background, so the first
|
|
@@ -976,42 +1241,97 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
976
1241
|
resolve();
|
|
977
1242
|
};
|
|
978
1243
|
});
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1244
|
+
// Shared by both branches below: whichever turn runs awaits this right
|
|
1245
|
+
// after starting, so executeStream's permission fallback still sees a
|
|
1246
|
+
// synchronous first-call failure.
|
|
1247
|
+
const raceFirstStep = async (resultPromise) => {
|
|
1248
|
+
const settledHere = resultPromise.then((result) => ({ result, error: undefined }), (error) => ({ result: undefined, error }));
|
|
1249
|
+
// A turn that ends without ever completing a step — an abort before
|
|
1250
|
+
// the first request, or a failure the engine gave up retrying — would
|
|
1251
|
+
// leave `firstStep` pending forever, so the settled outcome releases
|
|
1252
|
+
// the wait too.
|
|
1253
|
+
await Promise.race([firstStep, settledHere]);
|
|
1254
|
+
if (!firstStepSucceeded) {
|
|
1255
|
+
const outcome = await settledHere;
|
|
1256
|
+
if (outcome.error) {
|
|
1257
|
+
throw outcome.error;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
986
1260
|
};
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1261
|
+
const runTurn = (conversation, system, maxTokens, temperature, signal) => {
|
|
1262
|
+
const baseAdapter = createBedrockLoopAdapter({
|
|
1263
|
+
client: this.bedrockClient,
|
|
1264
|
+
streaming: true,
|
|
1265
|
+
region: this.region,
|
|
1266
|
+
maxSteps,
|
|
1267
|
+
buildCommandInput: (conv) => ({
|
|
1268
|
+
modelId: this.modelName || this.getDefaultModel(),
|
|
1269
|
+
messages: this.convertToAWSMessages(conv),
|
|
1270
|
+
// Converse rejects a blank block; transformParams may empty this.
|
|
1271
|
+
...(system ? { system: [{ text: system }] } : {}),
|
|
1272
|
+
inferenceConfig: {
|
|
1273
|
+
maxTokens,
|
|
1274
|
+
...(temperature !== undefined && { temperature }),
|
|
1275
|
+
},
|
|
1276
|
+
...(toolConfig ? { toolConfig } : {}),
|
|
1277
|
+
}),
|
|
1278
|
+
});
|
|
1279
|
+
const adapter = {
|
|
1280
|
+
...baseAdapter,
|
|
1281
|
+
executeStep: async (request, channel, signal) => {
|
|
1282
|
+
const stepResult = await baseAdapter.executeStep(request, channel, signal);
|
|
1283
|
+
firstStepSent();
|
|
1284
|
+
return stepResult;
|
|
1285
|
+
},
|
|
1286
|
+
};
|
|
1287
|
+
streamSpan.addEvent("stream.api_call", {
|
|
1288
|
+
"bedrock.tool_count": toolConfig?.tools?.length ?? 0,
|
|
1289
|
+
});
|
|
1290
|
+
return runAgenticLoop(adapter, conversation, {
|
|
1291
|
+
tools: this.toEngineTools(tools),
|
|
1292
|
+
abortSignal: signal,
|
|
1293
|
+
// `toEngineTools` adds no deadline of its own, so this is the only
|
|
1294
|
+
// per-tool bound on the turn. The engine defaults when it is absent.
|
|
1295
|
+
...(options.toolTimeoutMs !== undefined
|
|
1296
|
+
? { toolTimeoutMs: options.toolTimeoutMs }
|
|
1297
|
+
: {}),
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
const initialConversation = this.conversationHistory;
|
|
1301
|
+
// Turn-scoped abort: a V3 stream's `cancel()` (consumer broke out early)
|
|
1302
|
+
// must tear this turn's request down even when the caller passed no
|
|
1303
|
+
// signal. Chained so a caller abort still cascades.
|
|
1304
|
+
const turnAbort = new AbortController();
|
|
1305
|
+
if (options.abortSignal) {
|
|
1306
|
+
if (options.abortSignal.aborted) {
|
|
1307
|
+
turnAbort.abort();
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
options.abortSignal.addEventListener("abort", () => turnAbort.abort(), {
|
|
1311
|
+
once: true,
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// The request is already sent when this can throw; abort it so it is not
|
|
1316
|
+
// orphaned once the caller (e.g. the permission fallback) moves on.
|
|
1317
|
+
const { stream, capturedLoopResult } = await this.buildBedrockStreamTurn({
|
|
1318
|
+
initialConversation,
|
|
1319
|
+
systemPromptText,
|
|
1320
|
+
options,
|
|
1321
|
+
sampling,
|
|
1322
|
+
toolConfig,
|
|
1323
|
+
turnAbort,
|
|
1324
|
+
runTurn,
|
|
1325
|
+
raceFirstStep,
|
|
1326
|
+
}).catch((error) => {
|
|
1327
|
+
turnAbort.abort();
|
|
1328
|
+
throw error;
|
|
998
1329
|
});
|
|
999
1330
|
// The stream surfaces the same failure, so this settled view exists only
|
|
1000
1331
|
// so the turn's outcome can be read without a second unhandled rejection.
|
|
1001
|
-
|
|
1002
|
-
//
|
|
1003
|
-
|
|
1004
|
-
// `firstStep` pending forever, so the settled outcome releases the wait
|
|
1005
|
-
// too.
|
|
1006
|
-
await Promise.race([firstStep, settled]);
|
|
1007
|
-
if (!firstStepSucceeded) {
|
|
1008
|
-
// The turn ended before any send succeeded. Surface its error here so
|
|
1009
|
-
// executeStream's permission fallback still sees it synchronously.
|
|
1010
|
-
const outcome = await settled;
|
|
1011
|
-
if (outcome.error) {
|
|
1012
|
-
throw outcome.error;
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1332
|
+
// `capturedLoopResult` is undefined only when middleware blocked the turn
|
|
1333
|
+
// before `doStream` ran (correction 3) — every reader below tolerates it.
|
|
1334
|
+
const settled = (capturedLoopResult ?? Promise.resolve(undefined)).then((result) => ({ result, error: undefined }), (error) => ({ result: undefined, error }));
|
|
1015
1335
|
const streamEmitter = this.neurolink?.getEventEmitter();
|
|
1016
1336
|
const self = this;
|
|
1017
1337
|
const metadata = {
|
|
@@ -1022,21 +1342,6 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
1022
1342
|
const analyticsPromise = new Promise((resolve) => {
|
|
1023
1343
|
resolveAnalytics = resolve;
|
|
1024
1344
|
});
|
|
1025
|
-
const usageFromOutcome = (usage) => {
|
|
1026
|
-
const input = usage?.inputTokens ?? 0;
|
|
1027
|
-
const output = usage?.outputTokens ?? 0;
|
|
1028
|
-
const cacheRead = usage?.cacheReadTokens ?? 0;
|
|
1029
|
-
const cacheWrite = usage?.cacheWriteTokens ?? 0;
|
|
1030
|
-
return {
|
|
1031
|
-
input,
|
|
1032
|
-
output,
|
|
1033
|
-
// Cache reads/writes are billed tokens reported separately from
|
|
1034
|
-
// inputTokens — the total must include them.
|
|
1035
|
-
total: input + cacheRead + cacheWrite + output,
|
|
1036
|
-
...(cacheRead > 0 && { cacheReadTokens: cacheRead }),
|
|
1037
|
-
...(cacheWrite > 0 && { cacheCreationTokens: cacheWrite }),
|
|
1038
|
-
};
|
|
1039
|
-
};
|
|
1040
1345
|
// Bind analytics to the turn ending, not to the consumer draining the
|
|
1041
1346
|
// stream. A caller that awaits `result.analytics` without iterating
|
|
1042
1347
|
// `result.stream` would otherwise wait forever, because the generator
|
|
@@ -1044,7 +1349,7 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
1044
1349
|
// harmless; the first call wins.
|
|
1045
1350
|
void settled.then((outcome) => {
|
|
1046
1351
|
resolveAnalytics(createAnalytics(this.providerName, this.modelName || this.getDefaultModel(), {
|
|
1047
|
-
usage:
|
|
1352
|
+
usage: this.summarizeBedrockStreamUsage(outcome.result?.usage),
|
|
1048
1353
|
// createAnalytics derives toolCallCount from these. Passing usage
|
|
1049
1354
|
// alone left every streamed Bedrock turn reporting no tool calls,
|
|
1050
1355
|
// however many it made.
|
|
@@ -1078,7 +1383,7 @@ export class AmazonBedrockProvider extends BaseProvider {
|
|
|
1078
1383
|
if (outcome.error) {
|
|
1079
1384
|
streamErrored = true;
|
|
1080
1385
|
}
|
|
1081
|
-
const aggregatedUsage =
|
|
1386
|
+
const aggregatedUsage = self.summarizeBedrockStreamUsage(outcome.result?.usage);
|
|
1082
1387
|
if (outcome.result) {
|
|
1083
1388
|
self.conversationHistory = outcome.result.conversation;
|
|
1084
1389
|
metadata.finishReason = outcome.result.finishReason;
|