@llumiverse/drivers 0.23.0 → 0.24.0-dev.202601221707

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +141 -218
  2. package/lib/cjs/azure/azure_foundry.js +46 -2
  3. package/lib/cjs/azure/azure_foundry.js.map +1 -1
  4. package/lib/cjs/bedrock/index.js +236 -16
  5. package/lib/cjs/bedrock/index.js.map +1 -1
  6. package/lib/cjs/groq/index.js +115 -85
  7. package/lib/cjs/groq/index.js.map +1 -1
  8. package/lib/cjs/index.js +1 -0
  9. package/lib/cjs/index.js.map +1 -1
  10. package/lib/cjs/openai/index.js +310 -114
  11. package/lib/cjs/openai/index.js.map +1 -1
  12. package/lib/cjs/openai/openai_compatible.js +62 -0
  13. package/lib/cjs/openai/openai_compatible.js.map +1 -0
  14. package/lib/cjs/openai/openai_format.js +32 -39
  15. package/lib/cjs/openai/openai_format.js.map +1 -1
  16. package/lib/cjs/vertexai/index.js +165 -0
  17. package/lib/cjs/vertexai/index.js.map +1 -1
  18. package/lib/cjs/vertexai/models/claude.js +201 -3
  19. package/lib/cjs/vertexai/models/claude.js.map +1 -1
  20. package/lib/cjs/vertexai/models/gemini.js +59 -20
  21. package/lib/cjs/vertexai/models/gemini.js.map +1 -1
  22. package/lib/cjs/xai/index.js +10 -16
  23. package/lib/cjs/xai/index.js.map +1 -1
  24. package/lib/esm/azure/azure_foundry.js +46 -2
  25. package/lib/esm/azure/azure_foundry.js.map +1 -1
  26. package/lib/esm/bedrock/index.js +236 -17
  27. package/lib/esm/bedrock/index.js.map +1 -1
  28. package/lib/esm/groq/index.js +115 -85
  29. package/lib/esm/groq/index.js.map +1 -1
  30. package/lib/esm/index.js +1 -0
  31. package/lib/esm/index.js.map +1 -1
  32. package/lib/esm/openai/index.js +311 -115
  33. package/lib/esm/openai/index.js.map +1 -1
  34. package/lib/esm/openai/openai_compatible.js +55 -0
  35. package/lib/esm/openai/openai_compatible.js.map +1 -0
  36. package/lib/esm/openai/openai_format.js +32 -39
  37. package/lib/esm/openai/openai_format.js.map +1 -1
  38. package/lib/esm/vertexai/index.js +166 -1
  39. package/lib/esm/vertexai/index.js.map +1 -1
  40. package/lib/esm/vertexai/models/claude.js +199 -3
  41. package/lib/esm/vertexai/models/claude.js.map +1 -1
  42. package/lib/esm/vertexai/models/gemini.js +60 -21
  43. package/lib/esm/vertexai/models/gemini.js.map +1 -1
  44. package/lib/esm/xai/index.js +10 -16
  45. package/lib/esm/xai/index.js.map +1 -1
  46. package/lib/types/azure/azure_foundry.d.ts +7 -5
  47. package/lib/types/azure/azure_foundry.d.ts.map +1 -1
  48. package/lib/types/bedrock/index.d.ts +21 -1
  49. package/lib/types/bedrock/index.d.ts.map +1 -1
  50. package/lib/types/groq/index.d.ts.map +1 -1
  51. package/lib/types/index.d.ts +1 -0
  52. package/lib/types/index.d.ts.map +1 -1
  53. package/lib/types/openai/index.d.ts +13 -7
  54. package/lib/types/openai/index.d.ts.map +1 -1
  55. package/lib/types/openai/openai_compatible.d.ts +26 -0
  56. package/lib/types/openai/openai_compatible.d.ts.map +1 -0
  57. package/lib/types/openai/openai_format.d.ts +4 -2
  58. package/lib/types/openai/openai_format.d.ts.map +1 -1
  59. package/lib/types/vertexai/index.d.ts +15 -0
  60. package/lib/types/vertexai/index.d.ts.map +1 -1
  61. package/lib/types/vertexai/models/claude.d.ts +20 -0
  62. package/lib/types/vertexai/models/claude.d.ts.map +1 -1
  63. package/lib/types/vertexai/models/gemini.d.ts +1 -1
  64. package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
  65. package/lib/types/xai/index.d.ts +2 -3
  66. package/lib/types/xai/index.d.ts.map +1 -1
  67. package/package.json +12 -12
  68. package/src/azure/azure_foundry.ts +56 -7
  69. package/src/bedrock/index.ts +297 -26
  70. package/src/groq/index.ts +120 -94
  71. package/src/index.ts +1 -0
  72. package/src/openai/index.ts +363 -136
  73. package/src/openai/openai_compatible.ts +74 -0
  74. package/src/openai/openai_format.ts +44 -54
  75. package/src/vertexai/index.ts +205 -0
  76. package/src/vertexai/models/claude.ts +233 -3
  77. package/src/vertexai/models/gemini.ts +78 -27
  78. package/src/xai/index.ts +10 -17
@@ -2,7 +2,7 @@ import { DefaultAzureCredential, getBearerTokenProvider, TokenCredential } from
2
2
  import { AbstractDriver, AIModel, Completion, CompletionChunkObject, DriverOptions, EmbeddingsOptions, EmbeddingsResult, ExecutionOptions, getModelCapabilities, modelModalitiesToArray, Providers } from "@llumiverse/core";
3
3
  import { AIProjectClient, DeploymentUnion, ModelDeployment } from '@azure/ai-projects';
4
4
  import { isUnexpected } from "@azure-rest/ai-inference";
5
- import { ChatCompletionMessageParam } from "openai/resources";
5
+ import type OpenAI from "openai";
6
6
  import type {
7
7
  ChatCompletionsOutput,
8
8
  ChatCompletionsToolCall,
@@ -11,6 +11,9 @@ import type {
11
11
  import { AzureOpenAIDriver } from "../openai/azure_openai.js";
12
12
  import { createSseStream, NodeJSReadableStream } from "@azure/core-sse";
13
13
  import { formatOpenAILikeMultimodalPrompt } from "../openai/openai_format.js";
14
+
15
+ type ResponseInputItem = OpenAI.Responses.ResponseInputItem;
16
+ type EasyInputMessage = OpenAI.Responses.EasyInputMessage;
14
17
  export interface AzureFoundryDriverOptions extends DriverOptions {
15
18
  /**
16
19
  * The credentials to use to access Azure AI Foundry
@@ -27,12 +30,12 @@ export interface AzureFoundryInferencePrompt {
27
30
  }
28
31
 
29
32
  export interface AzureFoundryOpenAIPrompt {
30
- messages: ChatCompletionMessageParam[]
33
+ messages: ResponseInputItem[]
31
34
  }
32
35
 
33
36
  export type AzureFoundryPrompt = AzureFoundryInferencePrompt | AzureFoundryOpenAIPrompt
34
37
 
35
- export class AzureFoundryDriver extends AbstractDriver<AzureFoundryDriverOptions, ChatCompletionMessageParam[]> {
38
+ export class AzureFoundryDriver extends AbstractDriver<AzureFoundryDriverOptions, ResponseInputItem[]> {
36
39
  service: AIProjectClient;
37
40
  readonly provider = Providers.azure_foundry;
38
41
 
@@ -99,7 +102,7 @@ export class AzureFoundryDriver extends AbstractDriver<AzureFoundryDriverOptions
99
102
  return Promise.resolve(true);
100
103
  }
101
104
 
102
- async requestTextCompletion(prompt: ChatCompletionMessageParam[], options: ExecutionOptions): Promise<Completion> {
105
+ async requestTextCompletion(prompt: ResponseInputItem[], options: ExecutionOptions): Promise<Completion> {
103
106
  const { deploymentName } = parseAzureFoundryModelId(options.model);
104
107
  const model_options = options.model_options as any;
105
108
  const isOpenAI = await this.isOpenAIDeployment(options.model);
@@ -116,10 +119,12 @@ export class AzureFoundryDriver extends AbstractDriver<AzureFoundryDriverOptions
116
119
 
117
120
  } else {
118
121
  // Use the chat completions client from the inference operations
122
+ // Convert ResponseInputItem[] to ChatRequestMessage[] for non-OpenAI inference
123
+ const messages = convertToInferenceMessages(prompt);
119
124
  const chatClient = this.service.inference.chatCompletions({ apiVersion: this.INFERENCE_API_VERSION });
120
125
  response = await chatClient.post({
121
126
  body: {
122
- messages: prompt,
127
+ messages,
123
128
  max_tokens: model_options?.max_tokens,
124
129
  model: deploymentName,
125
130
  stream: true,
@@ -139,7 +144,7 @@ export class AzureFoundryDriver extends AbstractDriver<AzureFoundryDriverOptions
139
144
  }
140
145
  }
141
146
 
142
- async requestTextCompletionStream(prompt: ChatCompletionMessageParam[], options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>> {
147
+ async requestTextCompletionStream(prompt: ResponseInputItem[], options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>> {
143
148
  const { deploymentName } = parseAzureFoundryModelId(options.model);
144
149
  const model_options = options.model_options as any;
145
150
  const isOpenAI = await this.isOpenAIDeployment(options.model);
@@ -151,10 +156,12 @@ export class AzureFoundryDriver extends AbstractDriver<AzureFoundryDriverOptions
151
156
  const stream = await subDriver.requestTextCompletionStream(prompt, modifiedOptions);
152
157
  return stream;
153
158
  } else {
159
+ // Convert ResponseInputItem[] to ChatRequestMessage[] for non-OpenAI inference
160
+ const messages = convertToInferenceMessages(prompt);
154
161
  const chatClient = this.service.inference.chatCompletions({ apiVersion: this.INFERENCE_API_VERSION });
155
162
  const response = await chatClient.post({
156
163
  body: {
157
- messages: prompt,
164
+ messages,
158
165
  max_tokens: model_options?.max_tokens,
159
166
  model: deploymentName,
160
167
  stream: true,
@@ -456,3 +463,45 @@ export function parseAzureFoundryModelId(compositeId: string): { deploymentName:
456
463
  export function isCompositeModelId(modelId: string): boolean {
457
464
  return modelId.includes('::');
458
465
  }
466
+
467
+ /**
468
+ * Convert ResponseInputItem[] to ChatRequestMessage[] for Azure AI Inference API
469
+ */
470
+ function convertToInferenceMessages(items: ResponseInputItem[]): ChatRequestMessage[] {
471
+ const messages: ChatRequestMessage[] = [];
472
+
473
+ for (const item of items) {
474
+ // Handle EasyInputMessage (has role and content)
475
+ if ('role' in item && 'content' in item) {
476
+ const msg = item as EasyInputMessage;
477
+ let content: string;
478
+ if (typeof msg.content === 'string') {
479
+ content = msg.content;
480
+ } else if (Array.isArray(msg.content)) {
481
+ // Extract text from content array
482
+ content = msg.content
483
+ .filter((part): part is OpenAI.Responses.ResponseInputText => part.type === 'input_text')
484
+ .map(part => part.text)
485
+ .join('\n');
486
+ } else {
487
+ content = '';
488
+ }
489
+
490
+ messages.push({
491
+ role: msg.role as 'system' | 'user' | 'assistant',
492
+ content
493
+ });
494
+ }
495
+ // Handle function_call_output
496
+ else if ('type' in item && item.type === 'function_call_output') {
497
+ const output = item as OpenAI.Responses.ResponseInputItem.FunctionCallOutput;
498
+ messages.push({
499
+ role: 'tool',
500
+ content: typeof output.output === 'string' ? output.output : JSON.stringify(output.output),
501
+ tool_call_id: output.call_id,
502
+ } as ChatRequestMessage);
503
+ }
504
+ }
505
+
506
+ return messages;
507
+ }
@@ -2,17 +2,30 @@ import {
2
2
  Bedrock, CreateModelCustomizationJobCommand, FoundationModelSummary, GetModelCustomizationJobCommand,
3
3
  GetModelCustomizationJobCommandOutput, ModelCustomizationJobStatus, ModelModality, StopModelCustomizationJobCommand
4
4
  } from "@aws-sdk/client-bedrock";
5
- import { BedrockRuntime, ConverseRequest, ConverseResponse, ConverseStreamOutput, InferenceConfiguration, Tool } from "@aws-sdk/client-bedrock-runtime";
5
+ import { BedrockRuntime, ContentBlock, ConverseRequest, ConverseResponse, ConverseStreamOutput, InferenceConfiguration, Message, Tool } from "@aws-sdk/client-bedrock-runtime";
6
6
  import { S3Client } from "@aws-sdk/client-s3";
7
7
  import { AwsCredentialIdentity, Provider } from "@aws-sdk/types";
8
8
  import {
9
- AbstractDriver, AIModel, Completion, CompletionChunkObject, DataSource, DriverOptions, EmbeddingsOptions, EmbeddingsResult,
10
- ExecutionOptions, ExecutionTokenUsage, PromptSegment,
11
- TextFallbackOptions, ToolDefinition, ToolUse, TrainingJob, TrainingJobStatus, TrainingOptions,
12
- BedrockClaudeOptions, BedrockPalmyraOptions, BedrockGptOssOptions, getMaxTokensLimitBedrock, NovaCanvasOptions,
13
- modelModalitiesToArray, getModelCapabilities,
9
+ AbstractDriver, AIModel,
10
+ BedrockClaudeOptions,
11
+ BedrockGptOssOptions,
12
+ BedrockPalmyraOptions,
13
+ Completion, CompletionChunkObject, DataSource, DriverOptions, EmbeddingsOptions, EmbeddingsResult,
14
+ ExecutionOptions, ExecutionTokenUsage,
15
+ getMaxTokensLimitBedrock,
16
+ getModelCapabilities,
17
+ modelModalitiesToArray,
18
+ ModelOptions,
19
+ NovaCanvasOptions,
20
+ PromptSegment,
14
21
  StatelessExecutionOptions,
15
- ModelOptions
22
+ stripBinaryFromConversation,
23
+ truncateLargeTextInConversation,
24
+ deserializeBinaryFromStorage,
25
+ getConversationMeta,
26
+ incrementConversationTurn,
27
+ TextFallbackOptions, ToolDefinition, ToolUse, TrainingJob, TrainingJobStatus, TrainingOptions,
28
+ CompletionResult
16
29
  } from "@llumiverse/core";
17
30
  import { transformAsyncIterator } from "@llumiverse/core/async";
18
31
  import { formatNovaPrompt, NovaMessagesPrompt } from "@llumiverse/core/formatters";
@@ -22,9 +35,9 @@ import { formatNovaImageGenerationPayload, NovaImageGenerationTaskType } from ".
22
35
  import { forceUploadFile } from "./s3.js";
23
36
  import {
24
37
  formatTwelvelabsPegasusPrompt,
25
- TwelvelabsPegasusRequest,
26
38
  TwelvelabsMarengoRequest,
27
- TwelvelabsMarengoResponse
39
+ TwelvelabsMarengoResponse,
40
+ TwelvelabsPegasusRequest
28
41
  } from "./twelvelabs.js";
29
42
 
30
43
  const supportStreamingCache = new LRUCache<string, boolean>(4096);
@@ -114,7 +127,6 @@ export class BedrockDriver extends AbstractDriver<BedrockDriverOptions, BedrockP
114
127
  this._executor = new BedrockRuntime({
115
128
  region: this.options.region,
116
129
  credentials: this.options.credentials,
117
-
118
130
  });
119
131
  }
120
132
  return this._executor;
@@ -350,6 +362,91 @@ export class BedrockDriver extends AbstractDriver<BedrockDriverOptions, BedrockP
350
362
  return canStream;
351
363
  }
352
364
 
365
+ /**
366
+ * Build conversation context after streaming completion.
367
+ * Reconstructs the assistant message from accumulated results and applies stripping.
368
+ */
369
+ buildStreamingConversation(
370
+ prompt: BedrockPrompt,
371
+ result: unknown[],
372
+ toolUse: unknown[] | undefined,
373
+ options: ExecutionOptions
374
+ ): ConverseRequest | undefined {
375
+ // Only handle ConverseRequest prompts (not NovaMessagesPrompt or TwelvelabsPegasusRequest)
376
+ if (options.model.includes("canvas") || options.model.includes("twelvelabs.pegasus")) {
377
+ return undefined;
378
+ }
379
+
380
+ const conversePrompt = prompt as ConverseRequest;
381
+ const completionResults = result as CompletionResult[];
382
+
383
+ // Convert accumulated results to text content for assistant message
384
+ const textContent = completionResults
385
+ .map(r => {
386
+ switch (r.type) {
387
+ case 'text':
388
+ return r.value;
389
+ case 'json':
390
+ return typeof r.value === 'string' ? r.value : JSON.stringify(r.value);
391
+ case 'image':
392
+ // Skip images in conversation - they're in the result
393
+ return '';
394
+ default:
395
+ return String((r as any).value || '');
396
+ }
397
+ })
398
+ .join('');
399
+
400
+ // Deserialize any base64-encoded binary data back to Uint8Array
401
+ const incomingConversation = deserializeBinaryFromStorage(options.conversation) as ConverseRequest;
402
+
403
+ // Start with the conversation from options combined with the prompt
404
+ let conversation = updateConversation(incomingConversation, conversePrompt);
405
+
406
+ // Build assistant message content
407
+ const messageContent: any[] = [];
408
+ if (textContent) {
409
+ messageContent.push({ text: textContent });
410
+ }
411
+ // Add tool use blocks if present
412
+ if (toolUse && toolUse.length > 0) {
413
+ for (const tool of toolUse as ToolUse[]) {
414
+ messageContent.push({
415
+ toolUse: {
416
+ toolUseId: tool.id,
417
+ name: tool.tool_name,
418
+ input: tool.tool_input,
419
+ }
420
+ });
421
+ }
422
+ }
423
+
424
+ // Add assistant message
425
+ const assistantMessage: ConverseRequest = {
426
+ messages: [{
427
+ content: messageContent.length > 0 ? messageContent : [{ text: '' }],
428
+ role: "assistant"
429
+ }],
430
+ modelId: conversePrompt.modelId,
431
+ };
432
+ conversation = updateConversation(conversation, assistantMessage);
433
+
434
+ // Increment turn counter
435
+ conversation = incrementConversationTurn(conversation) as ConverseRequest;
436
+
437
+ // Apply stripping based on options
438
+ const currentTurn = getConversationMeta(conversation).turnNumber;
439
+ const stripOptions = {
440
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
441
+ currentTurn,
442
+ textMaxTokens: options.stripTextMaxTokens
443
+ };
444
+ let processedConversation = stripBinaryFromConversation(conversation, stripOptions);
445
+ processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
446
+
447
+ return processedConversation as ConverseRequest;
448
+ }
449
+
353
450
  async requestTextCompletion(prompt: BedrockPrompt, options: ExecutionOptions): Promise<Completion> {
354
451
  // Handle Twelvelabs Pegasus models
355
452
  if (options.model.includes("twelvelabs.pegasus")) {
@@ -358,7 +455,10 @@ export class BedrockDriver extends AbstractDriver<BedrockDriverOptions, BedrockP
358
455
 
359
456
  // Handle other Bedrock models that use Converse API
360
457
  const conversePrompt = prompt as ConverseRequest;
361
- let conversation = updateConversation(options.conversation as ConverseRequest, conversePrompt);
458
+
459
+ // Deserialize any base64-encoded binary data back to Uint8Array before API call
460
+ const incomingConversation = deserializeBinaryFromStorage(options.conversation) as ConverseRequest;
461
+ let conversation = updateConversation(incomingConversation, conversePrompt);
362
462
 
363
463
  const payload = this.preparePayload(conversation, options);
364
464
  const executor = this.getExecutor();
@@ -372,6 +472,9 @@ export class BedrockDriver extends AbstractDriver<BedrockDriverOptions, BedrockP
372
472
  modelId: conversePrompt.modelId,
373
473
  });
374
474
 
475
+ // Increment turn counter for deferred stripping
476
+ conversation = incrementConversationTurn(conversation) as ConverseRequest;
477
+
375
478
  let tool_use: ToolUse[] | undefined = undefined;
376
479
  //Get tool requests, we check tool use regardless of finish reason, as you can hit length and still get a valid response.
377
480
  tool_use = res.output?.message?.content?.reduce((tools: ToolUse[], c) => {
@@ -389,10 +492,22 @@ export class BedrockDriver extends AbstractDriver<BedrockDriverOptions, BedrockP
389
492
  tool_use = undefined;
390
493
  }
391
494
 
495
+ // Strip/serialize binary data based on options.stripImagesAfterTurns
496
+ const currentTurn = getConversationMeta(conversation).turnNumber;
497
+ const stripOptions = {
498
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
499
+ currentTurn,
500
+ textMaxTokens: options.stripTextMaxTokens
501
+ };
502
+ let processedConversation = stripBinaryFromConversation(conversation, stripOptions);
503
+
504
+ // Truncate large text content if configured
505
+ processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
506
+
392
507
  const completion = {
393
508
  ...this.getExtractedExecution(res, conversePrompt, options),
394
509
  original_response: options.include_original_response ? res : undefined,
395
- conversation: conversation,
510
+ conversation: processedConversation,
396
511
  tool_use: tool_use,
397
512
  };
398
513
 
@@ -496,7 +611,13 @@ export class BedrockDriver extends AbstractDriver<BedrockDriverOptions, BedrockP
496
611
 
497
612
  // Handle other Bedrock models that use Converse API
498
613
  const conversePrompt = prompt as ConverseRequest;
499
- const payload = this.preparePayload(conversePrompt, options);
614
+
615
+ // Include conversation history (same as non-streaming)
616
+ // Deserialize any base64-encoded binary data back to Uint8Array before API call
617
+ const incomingConversation = deserializeBinaryFromStorage(options.conversation) as ConverseRequest;
618
+ const conversation = updateConversation(incomingConversation, conversePrompt);
619
+
620
+ const payload = this.preparePayload(conversation, options);
500
621
  const executor = this.getExecutor();
501
622
  return executor.converseStream({
502
623
  ...payload,
@@ -642,22 +763,38 @@ export class BedrockDriver extends AbstractDriver<BedrockDriverOptions, BedrockP
642
763
  prompt.messages = converseJSONprefill(prompt.messages);
643
764
  }
644
765
 
766
+ // Clean undefined values from additionalField since AWS Bedrock requires valid JSON
767
+ // and will throw an exception for unrecognized parameters
768
+ const cleanedAdditionalFields = removeUndefinedValues(additionalField);
769
+ const cleanedModelOptions = removeUndefinedValues({
770
+ maxTokens: model_options.max_tokens,
771
+ temperature: model_options.temperature,
772
+ topP: model_options.top_p,
773
+ stopSequences: model_options.stop_sequence,
774
+ } satisfies InferenceConfiguration);
775
+
776
+ //Construct the final request payload
777
+ // We only add fields that are defined to avoid AWS errors
645
778
  const request: ConverseRequest = {
646
- messages: prompt.messages,
647
- system: prompt.system,
648
779
  modelId: options.model,
649
- inferenceConfig: {
650
- maxTokens: model_options.max_tokens,
651
- temperature: model_options.temperature,
652
- topP: model_options.top_p,
653
- stopSequences: model_options.stop_sequence,
654
- } satisfies InferenceConfiguration,
655
- additionalModelRequestFields: {
656
- ...additionalField,
657
- }
658
780
  };
659
781
 
660
- //Only add tools if they are defined and not empty
782
+ if (prompt.messages) {
783
+ request.messages = prompt.messages;
784
+ }
785
+
786
+ if (prompt.system) {
787
+ request.system = prompt.system;
788
+ }
789
+
790
+ if (Object.keys(cleanedModelOptions).length > 0) {
791
+ request.inferenceConfig = cleanedModelOptions
792
+ }
793
+
794
+ if (Object.keys(cleanedAdditionalFields).length > 0) {
795
+ request.additionalModelRequestFields = cleanedAdditionalFields;
796
+ }
797
+
661
798
  if (tool_defs?.length) {
662
799
  request.toolConfig = {
663
800
  tools: tool_defs,
@@ -1045,6 +1182,14 @@ export class BedrockDriver extends AbstractDriver<BedrockDriverOptions, BedrockP
1045
1182
  token_count: undefined
1046
1183
  };
1047
1184
  }
1185
+
1186
+ /**
1187
+ * Cleanup AWS SDK clients when the driver is evicted from the cache.
1188
+ */
1189
+ destroy(): void {
1190
+ this._executor?.destroy();
1191
+ this._service?.destroy();
1192
+ }
1048
1193
  }
1049
1194
 
1050
1195
  function jobInfo(job: GetModelCustomizationJobCommandOutput, jobId: string): TrainingJob {
@@ -1087,6 +1232,33 @@ function getToolDefinition(tool: ToolDefinition): Tool.ToolSpecMember {
1087
1232
  }
1088
1233
  }
1089
1234
 
1235
+ /**
1236
+ * Recursively removes undefined values from an object.
1237
+ * AWS Bedrock's additionalModelRequestFields must be valid JSON, and undefined is not valid JSON.
1238
+ * Any unrecognized parameters will cause an exception.
1239
+ */
1240
+ function removeUndefinedValues<T extends Record<string, any>>(obj: T): Partial<T> {
1241
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
1242
+ return obj;
1243
+ }
1244
+
1245
+ const cleaned: any = {};
1246
+ for (const [key, value] of Object.entries(obj)) {
1247
+ if (value !== undefined) {
1248
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
1249
+ const cleanedNested = removeUndefinedValues(value);
1250
+ // Only include nested objects if they have properties after cleaning
1251
+ if (Object.keys(cleanedNested).length > 0) {
1252
+ cleaned[key] = cleanedNested;
1253
+ }
1254
+ } else {
1255
+ cleaned[key] = value;
1256
+ }
1257
+ }
1258
+ }
1259
+ return cleaned;
1260
+ }
1261
+
1090
1262
  /**
1091
1263
  * Update the conversation messages
1092
1264
  * @param prompt
@@ -1097,13 +1269,112 @@ function updateConversation(conversation: ConverseRequest, prompt: ConverseReque
1097
1269
  const combinedMessages = [...(conversation?.messages || []), ...(prompt.messages || [])];
1098
1270
  const combinedSystem = prompt.system || conversation?.system;
1099
1271
 
1272
+ // Fix orphaned toolUse blocks before returning
1273
+ const fixedMessages = fixOrphanedToolUse(combinedMessages);
1274
+
1100
1275
  return {
1101
1276
  modelId: prompt?.modelId || conversation?.modelId,
1102
- messages: combinedMessages.length > 0 ? combinedMessages : [],
1277
+ messages: fixedMessages.length > 0 ? fixedMessages : [],
1103
1278
  system: combinedSystem && combinedSystem.length > 0 ? combinedSystem : undefined,
1104
1279
  };
1105
1280
  }
1106
1281
 
1282
+ /**
1283
+ * Fix orphaned toolUse blocks in the conversation.
1284
+ *
1285
+ * When an agent is stopped mid-tool-execution, the assistant message contains toolUse blocks
1286
+ * but no corresponding toolResult was added. The AWS Converse API requires that every toolUse
1287
+ * must be followed by a toolResult in the next user message.
1288
+ *
1289
+ * This function detects such cases and injects synthetic toolResult blocks indicating
1290
+ * the tools were interrupted, allowing the conversation to continue.
1291
+ */
1292
+ export function fixOrphanedToolUse(messages: Message[]): Message[] {
1293
+ if (messages.length < 2) return messages;
1294
+
1295
+ const result: Message[] = [];
1296
+
1297
+ for (let i = 0; i < messages.length; i++) {
1298
+ const current = messages[i];
1299
+ result.push(current);
1300
+
1301
+ // Check if this is an assistant message with toolUse blocks
1302
+ if (current.role === 'assistant' && current.content) {
1303
+ // Extract toolUse blocks using simple property check (same pattern as existing Bedrock code)
1304
+ const toolUseBlocks: Array<{ toolUseId: string; name: string }> = [];
1305
+ for (const block of current.content) {
1306
+ if (block.toolUse?.toolUseId) {
1307
+ toolUseBlocks.push({
1308
+ toolUseId: block.toolUse.toolUseId,
1309
+ name: block.toolUse.name ?? 'unknown'
1310
+ });
1311
+ }
1312
+ }
1313
+
1314
+ if (toolUseBlocks.length > 0) {
1315
+ // Check if the next message is a user message with matching toolResults
1316
+ const nextMessage = messages[i + 1];
1317
+
1318
+ if (nextMessage && nextMessage.role === 'user' && nextMessage.content) {
1319
+ // Get toolResult IDs from the next message using simple property check
1320
+ const toolResultIds = new Set<string>();
1321
+ for (const block of nextMessage.content) {
1322
+ if (block.toolResult?.toolUseId) {
1323
+ toolResultIds.add(block.toolResult.toolUseId);
1324
+ }
1325
+ }
1326
+
1327
+ // Find orphaned toolUse blocks (no matching toolResult)
1328
+ const orphanedToolUse = toolUseBlocks.filter(tu => !toolResultIds.has(tu.toolUseId));
1329
+
1330
+ if (orphanedToolUse.length > 0) {
1331
+ // Inject synthetic toolResults for orphaned toolUse
1332
+ const syntheticResults: ContentBlock[] = orphanedToolUse.map(tu => ({
1333
+ toolResult: {
1334
+ toolUseId: tu.toolUseId,
1335
+ content: [{
1336
+ text: `[Tool interrupted: The user stopped the operation before "${tu.name}" could execute.]`
1337
+ }]
1338
+ }
1339
+ }));
1340
+
1341
+ // Prepend synthetic results to the next user message
1342
+ const updatedNextMessage: Message = {
1343
+ ...nextMessage,
1344
+ content: [...syntheticResults, ...nextMessage.content]
1345
+ };
1346
+
1347
+ // Replace the next message in our iteration
1348
+ messages[i + 1] = updatedNextMessage;
1349
+ }
1350
+ } else if (nextMessage && nextMessage.role === 'user' && !nextMessage.content) {
1351
+ // Next message is a user message but has no content
1352
+ // We need to add toolResults
1353
+ const syntheticResults: ContentBlock[] = toolUseBlocks.map(tu => ({
1354
+ toolResult: {
1355
+ toolUseId: tu.toolUseId,
1356
+ content: [{
1357
+ text: `[Tool interrupted: The user stopped the operation before "${tu.name}" could execute.]`
1358
+ }]
1359
+ }
1360
+ }));
1361
+
1362
+ const updatedNextMessage: Message = {
1363
+ role: 'user',
1364
+ content: syntheticResults
1365
+ };
1366
+
1367
+ messages[i + 1] = updatedNextMessage;
1368
+ }
1369
+ // Note: If there's no nextMessage, we leave the conversation as-is.
1370
+ // The toolUse blocks are expected to be there - the next turn will provide toolResults.
1371
+ }
1372
+ }
1373
+ }
1374
+
1375
+ return result;
1376
+ }
1377
+
1107
1378
  function formatAmazonModalities(modalities: ModelModality[]): string[] {
1108
1379
  const standardizedModalities: string[] = [];
1109
1380
  for (const modality of modalities) {