@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.
- package/README.md +141 -218
- package/lib/cjs/azure/azure_foundry.js +46 -2
- package/lib/cjs/azure/azure_foundry.js.map +1 -1
- package/lib/cjs/bedrock/index.js +236 -16
- package/lib/cjs/bedrock/index.js.map +1 -1
- package/lib/cjs/groq/index.js +115 -85
- package/lib/cjs/groq/index.js.map +1 -1
- package/lib/cjs/index.js +1 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/openai/index.js +310 -114
- package/lib/cjs/openai/index.js.map +1 -1
- package/lib/cjs/openai/openai_compatible.js +62 -0
- package/lib/cjs/openai/openai_compatible.js.map +1 -0
- package/lib/cjs/openai/openai_format.js +32 -39
- package/lib/cjs/openai/openai_format.js.map +1 -1
- package/lib/cjs/vertexai/index.js +165 -0
- package/lib/cjs/vertexai/index.js.map +1 -1
- package/lib/cjs/vertexai/models/claude.js +201 -3
- package/lib/cjs/vertexai/models/claude.js.map +1 -1
- package/lib/cjs/vertexai/models/gemini.js +59 -20
- package/lib/cjs/vertexai/models/gemini.js.map +1 -1
- package/lib/cjs/xai/index.js +10 -16
- package/lib/cjs/xai/index.js.map +1 -1
- package/lib/esm/azure/azure_foundry.js +46 -2
- package/lib/esm/azure/azure_foundry.js.map +1 -1
- package/lib/esm/bedrock/index.js +236 -17
- package/lib/esm/bedrock/index.js.map +1 -1
- package/lib/esm/groq/index.js +115 -85
- package/lib/esm/groq/index.js.map +1 -1
- package/lib/esm/index.js +1 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/openai/index.js +311 -115
- package/lib/esm/openai/index.js.map +1 -1
- package/lib/esm/openai/openai_compatible.js +55 -0
- package/lib/esm/openai/openai_compatible.js.map +1 -0
- package/lib/esm/openai/openai_format.js +32 -39
- package/lib/esm/openai/openai_format.js.map +1 -1
- package/lib/esm/vertexai/index.js +166 -1
- package/lib/esm/vertexai/index.js.map +1 -1
- package/lib/esm/vertexai/models/claude.js +199 -3
- package/lib/esm/vertexai/models/claude.js.map +1 -1
- package/lib/esm/vertexai/models/gemini.js +60 -21
- package/lib/esm/vertexai/models/gemini.js.map +1 -1
- package/lib/esm/xai/index.js +10 -16
- package/lib/esm/xai/index.js.map +1 -1
- package/lib/types/azure/azure_foundry.d.ts +7 -5
- package/lib/types/azure/azure_foundry.d.ts.map +1 -1
- package/lib/types/bedrock/index.d.ts +21 -1
- package/lib/types/bedrock/index.d.ts.map +1 -1
- package/lib/types/groq/index.d.ts.map +1 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/openai/index.d.ts +13 -7
- package/lib/types/openai/index.d.ts.map +1 -1
- package/lib/types/openai/openai_compatible.d.ts +26 -0
- package/lib/types/openai/openai_compatible.d.ts.map +1 -0
- package/lib/types/openai/openai_format.d.ts +4 -2
- package/lib/types/openai/openai_format.d.ts.map +1 -1
- package/lib/types/vertexai/index.d.ts +15 -0
- package/lib/types/vertexai/index.d.ts.map +1 -1
- package/lib/types/vertexai/models/claude.d.ts +20 -0
- package/lib/types/vertexai/models/claude.d.ts.map +1 -1
- package/lib/types/vertexai/models/gemini.d.ts +1 -1
- package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
- package/lib/types/xai/index.d.ts +2 -3
- package/lib/types/xai/index.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/azure/azure_foundry.ts +56 -7
- package/src/bedrock/index.ts +297 -26
- package/src/groq/index.ts +120 -94
- package/src/index.ts +1 -0
- package/src/openai/index.ts +363 -136
- package/src/openai/openai_compatible.ts +74 -0
- package/src/openai/openai_format.ts +44 -54
- package/src/vertexai/index.ts +205 -0
- package/src/vertexai/models/claude.ts +233 -3
- package/src/vertexai/models/gemini.ts +78 -27
- 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
|
|
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:
|
|
33
|
+
messages: ResponseInputItem[]
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export type AzureFoundryPrompt = AzureFoundryInferencePrompt | AzureFoundryOpenAIPrompt
|
|
34
37
|
|
|
35
|
-
export class AzureFoundryDriver extends AbstractDriver<AzureFoundryDriverOptions,
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
+
}
|
package/src/bedrock/index.ts
CHANGED
|
@@ -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,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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) {
|