d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
957 lines
32 KiB
TypeScript
957 lines
32 KiB
TypeScript
import {
|
|
type Message as BedrockMessage,
|
|
BedrockRuntimeClient,
|
|
type BedrockRuntimeClientConfig,
|
|
type ContentBlock,
|
|
type ConversationRole,
|
|
ConverseCommand,
|
|
type ConverseResponse,
|
|
ConverseStreamCommand,
|
|
type SystemContentBlock,
|
|
type Tool,
|
|
type ToolConfiguration,
|
|
type ToolResultBlock,
|
|
type ToolUseBlock,
|
|
} from '@aws-sdk/client-bedrock-runtime'
|
|
import { createLogger } from '@sim/logger'
|
|
import { getErrorMessage, toError } from '@sim/utils/errors'
|
|
import type { IterationToolCall, StreamingExecution } from '@/executor/types'
|
|
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
|
import { buildBedrockMessageContent } from '@/providers/attachments'
|
|
import {
|
|
checkForForcedToolUsage,
|
|
createReadableStreamFromBedrockStream,
|
|
generateToolUseId,
|
|
getBedrockInferenceProfileId,
|
|
} from '@/providers/bedrock/utils'
|
|
import { getCachedProviderClient } from '@/providers/client-cache'
|
|
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
|
|
import { createStreamingExecution } from '@/providers/streaming-execution'
|
|
import { enrichLastModelSegment } from '@/providers/trace-enrichment'
|
|
import type {
|
|
FunctionCallResponse,
|
|
ProviderConfig,
|
|
ProviderRequest,
|
|
ProviderResponse,
|
|
TimeSegment,
|
|
} from '@/providers/types'
|
|
import { ProviderError } from '@/providers/types'
|
|
import {
|
|
calculateCost,
|
|
prepareToolExecution,
|
|
prepareToolsWithUsageControl,
|
|
sumToolCosts,
|
|
} from '@/providers/utils'
|
|
import { executeTool } from '@/tools'
|
|
|
|
const logger = createLogger('BedrockProvider')
|
|
|
|
function enrichLastModelSegmentFromBedrockResponse(
|
|
timeSegments: TimeSegment[],
|
|
response: ConverseResponse,
|
|
extras: { model: string }
|
|
): void {
|
|
const blocks: ContentBlock[] = response.output?.message?.content ?? []
|
|
|
|
const assistantText = blocks
|
|
.filter((b): b is ContentBlock & { text: string } => 'text' in b && typeof b.text === 'string')
|
|
.map((b) => b.text)
|
|
.join('\n')
|
|
const assistantContent = assistantText.length > 0 ? assistantText : undefined
|
|
|
|
const toolCalls: IterationToolCall[] = blocks
|
|
.filter((b): b is ContentBlock & { toolUse: ToolUseBlock } => 'toolUse' in b && !!b.toolUse)
|
|
.map((b) => {
|
|
const input = b.toolUse.input
|
|
return {
|
|
id: b.toolUse.toolUseId ?? '',
|
|
name: b.toolUse.name ?? '',
|
|
arguments:
|
|
input && typeof input === 'object' && !Array.isArray(input)
|
|
? (input as Record<string, unknown>)
|
|
: {},
|
|
}
|
|
})
|
|
|
|
const inputTokens = response.usage?.inputTokens
|
|
const outputTokens = response.usage?.outputTokens
|
|
|
|
let cost: { input: number; output: number; total: number } | undefined
|
|
if (typeof inputTokens === 'number' && typeof outputTokens === 'number') {
|
|
const full = calculateCost(extras.model, inputTokens, outputTokens)
|
|
cost = { input: full.input, output: full.output, total: full.total }
|
|
}
|
|
|
|
enrichLastModelSegment(timeSegments, {
|
|
assistantContent,
|
|
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
finishReason: response.stopReason ?? undefined,
|
|
tokens:
|
|
inputTokens !== undefined || outputTokens !== undefined
|
|
? {
|
|
input: inputTokens,
|
|
output: outputTokens,
|
|
total:
|
|
typeof inputTokens === 'number' && typeof outputTokens === 'number'
|
|
? inputTokens + outputTokens
|
|
: undefined,
|
|
}
|
|
: undefined,
|
|
cost,
|
|
provider: 'aws.bedrock',
|
|
})
|
|
}
|
|
|
|
export const bedrockProvider: ProviderConfig = {
|
|
id: 'bedrock',
|
|
name: 'AWS Bedrock',
|
|
description: 'AWS Bedrock foundation models',
|
|
version: '1.0.0',
|
|
models: getProviderModels('bedrock'),
|
|
defaultModel: getProviderDefaultModel('bedrock'),
|
|
|
|
executeRequest: async (
|
|
request: ProviderRequest
|
|
): Promise<ProviderResponse | StreamingExecution> => {
|
|
const region = request.bedrockRegion || 'us-east-1'
|
|
const bedrockModelId = getBedrockInferenceProfileId(request.model, region)
|
|
|
|
logger.info('Bedrock request', {
|
|
requestModel: request.model,
|
|
inferenceProfileId: bedrockModelId,
|
|
region,
|
|
})
|
|
|
|
const hasAccessKey = Boolean(request.bedrockAccessKeyId)
|
|
const hasSecretKey = Boolean(request.bedrockSecretKey)
|
|
if (hasAccessKey !== hasSecretKey) {
|
|
throw new Error(
|
|
'Both bedrockAccessKeyId and bedrockSecretKey must be provided together. ' +
|
|
'Provide both for explicit credentials, or omit both to use the AWS default credential chain.'
|
|
)
|
|
}
|
|
|
|
const clientConfig: BedrockRuntimeClientConfig = { region }
|
|
if (request.bedrockAccessKeyId && request.bedrockSecretKey) {
|
|
clientConfig.credentials = {
|
|
accessKeyId: request.bedrockAccessKeyId,
|
|
secretAccessKey: request.bedrockSecretKey,
|
|
}
|
|
}
|
|
|
|
// Key on the full credential (access key id + secret) so a corrected secret
|
|
// under the same access key id yields a fresh client rather than a stale one.
|
|
const credentialKey =
|
|
request.bedrockAccessKeyId && request.bedrockSecretKey
|
|
? `${request.bedrockAccessKeyId}:${request.bedrockSecretKey}`
|
|
: 'default-chain'
|
|
const client = getCachedProviderClient(
|
|
`bedrock::${region}::${credentialKey}`,
|
|
() => new BedrockRuntimeClient(clientConfig)
|
|
)
|
|
|
|
const messages: BedrockMessage[] = []
|
|
const systemContent: SystemContentBlock[] = []
|
|
|
|
if (request.systemPrompt) {
|
|
systemContent.push({ text: request.systemPrompt })
|
|
}
|
|
|
|
if (request.context) {
|
|
messages.push({
|
|
role: 'user' as ConversationRole,
|
|
content: [{ text: request.context }],
|
|
})
|
|
}
|
|
|
|
if (request.messages) {
|
|
for (const msg of request.messages) {
|
|
if (msg.role === 'function' || msg.role === 'tool') {
|
|
const toolResultBlock: ToolResultBlock = {
|
|
toolUseId: msg.tool_call_id || msg.name || generateToolUseId('tool'),
|
|
content: [{ text: msg.content || '' }],
|
|
}
|
|
messages.push({
|
|
role: 'user' as ConversationRole,
|
|
content: [{ toolResult: toolResultBlock }],
|
|
})
|
|
} else if (msg.function_call || msg.tool_calls) {
|
|
const toolCall = msg.function_call || msg.tool_calls?.[0]?.function
|
|
if (toolCall) {
|
|
const toolUseBlock: ToolUseBlock = {
|
|
toolUseId: msg.tool_calls?.[0]?.id || generateToolUseId(toolCall.name),
|
|
name: toolCall.name,
|
|
input: JSON.parse(toolCall.arguments),
|
|
}
|
|
messages.push({
|
|
role: 'assistant' as ConversationRole,
|
|
content: [{ toolUse: toolUseBlock }],
|
|
})
|
|
}
|
|
} else {
|
|
const role: ConversationRole = msg.role === 'assistant' ? 'assistant' : 'user'
|
|
const content = buildBedrockMessageContent(msg.content, msg.files, 'bedrock')
|
|
messages.push({
|
|
role,
|
|
// double-cast-allowed: shared attachment builder emits Bedrock Converse content blocks while keeping provider-neutral attachment types
|
|
content: content as unknown as ContentBlock[],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if (messages.length === 0) {
|
|
messages.push({
|
|
role: 'user' as ConversationRole,
|
|
content: [{ text: request.systemPrompt || 'Hello' }],
|
|
})
|
|
systemContent.length = 0
|
|
}
|
|
|
|
let structuredOutputTool: Tool | undefined
|
|
const structuredOutputToolName = 'structured_output'
|
|
|
|
if (request.responseFormat) {
|
|
const schema = request.responseFormat.schema || request.responseFormat
|
|
const schemaName = request.responseFormat.name || 'response'
|
|
|
|
structuredOutputTool = {
|
|
toolSpec: {
|
|
name: structuredOutputToolName,
|
|
description: `Output the response as structured JSON matching the ${schemaName} schema. You MUST call this tool to provide your final response.`,
|
|
inputSchema: {
|
|
json: schema,
|
|
},
|
|
},
|
|
}
|
|
|
|
logger.info(`Using Tool Use approach for structured outputs: ${schemaName}`)
|
|
}
|
|
|
|
let bedrockTools: Tool[] | undefined
|
|
let toolChoice: any = { auto: {} }
|
|
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
|
|
|
|
if (request.tools?.length) {
|
|
bedrockTools = request.tools.map((tool) => ({
|
|
toolSpec: {
|
|
name: tool.id,
|
|
description: tool.description,
|
|
inputSchema: {
|
|
json: {
|
|
type: 'object',
|
|
properties: tool.parameters.properties,
|
|
required: tool.parameters.required,
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
try {
|
|
preparedTools = prepareToolsWithUsageControl(
|
|
bedrockTools.map((t) => ({
|
|
name: t.toolSpec?.name || '',
|
|
description: t.toolSpec?.description || '',
|
|
input_schema: t.toolSpec?.inputSchema?.json,
|
|
})),
|
|
request.tools,
|
|
logger,
|
|
'bedrock'
|
|
)
|
|
|
|
const { tools: filteredTools, toolChoice: tc } = preparedTools
|
|
|
|
if (filteredTools?.length) {
|
|
bedrockTools = filteredTools.map((t: any) => ({
|
|
toolSpec: {
|
|
name: t.name,
|
|
description: t.description,
|
|
inputSchema: { json: t.input_schema },
|
|
},
|
|
}))
|
|
|
|
if (typeof tc === 'object' && tc !== null) {
|
|
if (tc.type === 'tool' && tc.name) {
|
|
toolChoice = { tool: { name: tc.name } }
|
|
logger.info(`Using Bedrock tool_choice format: force tool "${tc.name}"`)
|
|
} else if (tc.type === 'function' && tc.function?.name) {
|
|
toolChoice = { tool: { name: tc.function.name } }
|
|
logger.info(`Using Bedrock tool_choice format: force tool "${tc.function.name}"`)
|
|
} else if (tc.type === 'any') {
|
|
toolChoice = { any: {} }
|
|
logger.info('Using Bedrock tool_choice format: any tool')
|
|
} else {
|
|
toolChoice = { auto: {} }
|
|
}
|
|
} else if (tc === 'none') {
|
|
toolChoice = undefined
|
|
bedrockTools = undefined
|
|
} else {
|
|
toolChoice = { auto: {} }
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error in prepareToolsWithUsageControl:', { error })
|
|
toolChoice = { auto: {} }
|
|
}
|
|
} else if (structuredOutputTool) {
|
|
bedrockTools = [structuredOutputTool]
|
|
toolChoice = { tool: { name: structuredOutputToolName } }
|
|
logger.info('Using structured_output tool as only tool (forced)')
|
|
}
|
|
|
|
const hasToolContentInMessages = messages.some((msg) =>
|
|
msg.content?.some(
|
|
(block) =>
|
|
('toolUse' in block && block.toolUse) || ('toolResult' in block && block.toolResult)
|
|
)
|
|
)
|
|
|
|
const toolConfig: ToolConfiguration | undefined = bedrockTools?.length
|
|
? {
|
|
tools: bedrockTools,
|
|
toolChoice,
|
|
}
|
|
: hasToolContentInMessages && request.tools?.length
|
|
? {
|
|
tools: request.tools.map((tool) => ({
|
|
toolSpec: {
|
|
name: tool.id,
|
|
description: tool.description,
|
|
inputSchema: {
|
|
json: {
|
|
type: 'object',
|
|
properties: tool.parameters.properties,
|
|
required: tool.parameters.required,
|
|
},
|
|
},
|
|
},
|
|
})),
|
|
toolChoice: { auto: {} },
|
|
}
|
|
: undefined
|
|
|
|
if (hasToolContentInMessages && !toolConfig) {
|
|
throw new Error(
|
|
'Messages contain tool use/result blocks but no tools were provided. ' +
|
|
'Bedrock requires toolConfig when processing messages with tool content.'
|
|
)
|
|
}
|
|
|
|
const systemPromptWithSchema = systemContent
|
|
|
|
const inferenceConfig: { temperature: number; maxTokens?: number } = {
|
|
temperature: Number.parseFloat(String(request.temperature ?? 0.7)),
|
|
}
|
|
if (request.maxTokens != null) {
|
|
inferenceConfig.maxTokens = Number.parseInt(String(request.maxTokens))
|
|
}
|
|
|
|
const shouldStreamToolCalls = request.streamToolCalls ?? false
|
|
|
|
if (request.stream && (!bedrockTools || bedrockTools.length === 0)) {
|
|
logger.info('Using streaming response for Bedrock request (no tools)')
|
|
|
|
const providerStartTime = Date.now()
|
|
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
|
|
|
const command = new ConverseStreamCommand({
|
|
modelId: bedrockModelId,
|
|
messages,
|
|
system: systemPromptWithSchema.length > 0 ? systemPromptWithSchema : undefined,
|
|
inferenceConfig,
|
|
})
|
|
|
|
const streamResponse = await client.send(
|
|
command,
|
|
request.abortSignal ? { abortSignal: request.abortSignal } : undefined
|
|
)
|
|
|
|
if (!streamResponse.stream) {
|
|
throw new Error('No stream returned from Bedrock')
|
|
}
|
|
|
|
const bedrockStream = streamResponse.stream
|
|
const streamingResult = createStreamingExecution({
|
|
model: request.model,
|
|
providerStartTime,
|
|
providerStartTimeISO,
|
|
timing: { kind: 'simple', segmentName: request.model },
|
|
initialTokens: { input: 0, output: 0, total: 0 },
|
|
initialCost: { total: 0.0, input: 0.0, output: 0.0 },
|
|
isStreaming: true,
|
|
createStream: ({ output, finalizeTiming }) =>
|
|
createReadableStreamFromBedrockStream(bedrockStream, (content, usage) => {
|
|
output.content = content
|
|
output.tokens = {
|
|
input: usage.inputTokens,
|
|
output: usage.outputTokens,
|
|
total: usage.inputTokens + usage.outputTokens,
|
|
}
|
|
|
|
const costResult = calculateCost(request.model, usage.inputTokens, usage.outputTokens)
|
|
output.cost = {
|
|
input: costResult.input,
|
|
output: costResult.output,
|
|
total: costResult.total,
|
|
}
|
|
|
|
finalizeTiming()
|
|
}),
|
|
})
|
|
|
|
return streamingResult
|
|
}
|
|
|
|
const providerStartTime = Date.now()
|
|
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
|
|
|
try {
|
|
const initialCallTime = Date.now()
|
|
const originalToolChoice = toolChoice
|
|
const forcedTools = preparedTools?.forcedTools || []
|
|
let usedForcedTools: string[] = []
|
|
|
|
const command = new ConverseCommand({
|
|
modelId: bedrockModelId,
|
|
messages,
|
|
system: systemPromptWithSchema.length > 0 ? systemPromptWithSchema : undefined,
|
|
inferenceConfig,
|
|
toolConfig,
|
|
})
|
|
|
|
let currentResponse = await client.send(
|
|
command,
|
|
request.abortSignal ? { abortSignal: request.abortSignal } : undefined
|
|
)
|
|
const firstResponseTime = Date.now() - initialCallTime
|
|
|
|
let content = ''
|
|
let hasExtractedStructuredOutput = false
|
|
if (currentResponse.output?.message?.content) {
|
|
const structuredOutputCall = currentResponse.output.message.content.find(
|
|
(block): block is ContentBlock & { toolUse: ToolUseBlock } =>
|
|
'toolUse' in block && block.toolUse?.name === structuredOutputToolName
|
|
)
|
|
|
|
if (structuredOutputCall && structuredOutputTool) {
|
|
content = JSON.stringify(structuredOutputCall.toolUse.input, null, 2)
|
|
hasExtractedStructuredOutput = true
|
|
logger.info('Extracted structured output from tool call')
|
|
} else {
|
|
const textBlocks = currentResponse.output.message.content.filter(
|
|
(block): block is ContentBlock & { text: string } => 'text' in block
|
|
)
|
|
content = textBlocks.map((block) => block.text).join('\n')
|
|
}
|
|
}
|
|
|
|
const tokens = {
|
|
input: currentResponse.usage?.inputTokens || 0,
|
|
output: currentResponse.usage?.outputTokens || 0,
|
|
total:
|
|
(currentResponse.usage?.inputTokens || 0) + (currentResponse.usage?.outputTokens || 0),
|
|
}
|
|
|
|
const initialCost = calculateCost(
|
|
request.model,
|
|
currentResponse.usage?.inputTokens || 0,
|
|
currentResponse.usage?.outputTokens || 0
|
|
)
|
|
const cost = {
|
|
input: initialCost.input,
|
|
output: initialCost.output,
|
|
total: initialCost.total,
|
|
pricing: initialCost.pricing,
|
|
}
|
|
|
|
const toolCalls: FunctionCallResponse[] = []
|
|
const toolResults: Record<string, unknown>[] = []
|
|
const currentMessages = [...messages]
|
|
let iterationCount = 0
|
|
let hasUsedForcedTool = false
|
|
let modelTime = firstResponseTime
|
|
let toolsTime = 0
|
|
|
|
const timeSegments: TimeSegment[] = [
|
|
{
|
|
type: 'model',
|
|
name: request.model,
|
|
startTime: initialCallTime,
|
|
endTime: initialCallTime + firstResponseTime,
|
|
duration: firstResponseTime,
|
|
},
|
|
]
|
|
|
|
enrichLastModelSegmentFromBedrockResponse(timeSegments, currentResponse, {
|
|
model: request.model,
|
|
})
|
|
|
|
const initialToolUseContentBlocks = (currentResponse.output?.message?.content || []).filter(
|
|
(block): block is ContentBlock & { toolUse: ToolUseBlock } => 'toolUse' in block
|
|
)
|
|
const toolUseBlocks = initialToolUseContentBlocks.map((block) => ({
|
|
name: block.toolUse.name || '',
|
|
}))
|
|
|
|
const firstCheckResult = checkForForcedToolUsage(
|
|
toolUseBlocks,
|
|
originalToolChoice,
|
|
forcedTools,
|
|
usedForcedTools
|
|
)
|
|
if (firstCheckResult) {
|
|
hasUsedForcedTool = firstCheckResult.hasUsedForcedTool
|
|
usedForcedTools = firstCheckResult.usedForcedTools
|
|
}
|
|
|
|
while (iterationCount < MAX_TOOL_ITERATIONS) {
|
|
const textContentBlocks = (currentResponse.output?.message?.content || []).filter(
|
|
(block): block is ContentBlock & { text: string } => 'text' in block
|
|
)
|
|
const textContent = textContentBlocks.map((block) => block.text).join('\n')
|
|
|
|
if (textContent) {
|
|
content = textContent
|
|
}
|
|
|
|
const toolUseContentBlocks = (currentResponse.output?.message?.content || []).filter(
|
|
(block): block is ContentBlock & { toolUse: ToolUseBlock } => 'toolUse' in block
|
|
)
|
|
const currentToolUses = toolUseContentBlocks.map((block) => block.toolUse)
|
|
|
|
if (!currentToolUses || currentToolUses.length === 0) {
|
|
break
|
|
}
|
|
|
|
const toolsStartTime = Date.now()
|
|
|
|
const toolExecutionPromises = currentToolUses.map(async (toolUse: ToolUseBlock) => {
|
|
const toolCallStartTime = Date.now()
|
|
const toolName = toolUse.name || ''
|
|
const toolArgs = (toolUse.input as Record<string, any>) || {}
|
|
const toolUseId = toolUse.toolUseId || generateToolUseId(toolName)
|
|
|
|
try {
|
|
const tool = request.tools?.find((t) => t.id === toolName)
|
|
if (!tool) return null
|
|
|
|
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
|
const result = await executeTool(toolName, executionParams, {
|
|
signal: request.abortSignal,
|
|
})
|
|
const toolCallEndTime = Date.now()
|
|
|
|
return {
|
|
toolUseId,
|
|
toolName,
|
|
toolArgs,
|
|
toolParams,
|
|
result,
|
|
startTime: toolCallStartTime,
|
|
endTime: toolCallEndTime,
|
|
duration: toolCallEndTime - toolCallStartTime,
|
|
}
|
|
} catch (error) {
|
|
const toolCallEndTime = Date.now()
|
|
logger.error('Error processing tool call:', { error, toolName })
|
|
|
|
return {
|
|
toolUseId,
|
|
toolName,
|
|
toolArgs,
|
|
toolParams: {},
|
|
result: {
|
|
success: false,
|
|
output: undefined,
|
|
error: getErrorMessage(error, 'Tool execution failed'),
|
|
},
|
|
startTime: toolCallStartTime,
|
|
endTime: toolCallEndTime,
|
|
duration: toolCallEndTime - toolCallStartTime,
|
|
}
|
|
}
|
|
})
|
|
|
|
const executionResults = await Promise.allSettled(toolExecutionPromises)
|
|
|
|
const assistantContent: ContentBlock[] = currentToolUses.map((toolUse: ToolUseBlock) => ({
|
|
toolUse: {
|
|
toolUseId: toolUse.toolUseId,
|
|
name: toolUse.name,
|
|
input: toolUse.input,
|
|
},
|
|
}))
|
|
currentMessages.push({
|
|
role: 'assistant' as ConversationRole,
|
|
content: assistantContent,
|
|
})
|
|
|
|
const toolResultContent: ContentBlock[] = []
|
|
|
|
for (const settledResult of executionResults) {
|
|
if (settledResult.status === 'rejected' || !settledResult.value) continue
|
|
|
|
const {
|
|
toolUseId,
|
|
toolName,
|
|
toolArgs,
|
|
toolParams,
|
|
result,
|
|
startTime,
|
|
endTime,
|
|
duration,
|
|
} = settledResult.value
|
|
|
|
timeSegments.push({
|
|
type: 'tool',
|
|
name: toolName,
|
|
startTime,
|
|
endTime,
|
|
duration,
|
|
})
|
|
|
|
let resultContent: any
|
|
if (result.success) {
|
|
toolResults.push(result.output!)
|
|
resultContent = result.output
|
|
} else {
|
|
resultContent = {
|
|
error: true,
|
|
message: result.error || 'Tool execution failed',
|
|
tool: toolName,
|
|
}
|
|
}
|
|
|
|
toolCalls.push({
|
|
name: toolName,
|
|
arguments: toolParams,
|
|
startTime: new Date(startTime).toISOString(),
|
|
endTime: new Date(endTime).toISOString(),
|
|
duration,
|
|
result: resultContent,
|
|
success: result.success,
|
|
})
|
|
|
|
const toolResultBlock: ToolResultBlock = {
|
|
toolUseId,
|
|
content: [{ text: JSON.stringify(resultContent) }],
|
|
}
|
|
toolResultContent.push({ toolResult: toolResultBlock })
|
|
}
|
|
|
|
if (toolResultContent.length > 0) {
|
|
currentMessages.push({
|
|
role: 'user' as ConversationRole,
|
|
content: toolResultContent,
|
|
})
|
|
}
|
|
|
|
const thisToolsTime = Date.now() - toolsStartTime
|
|
toolsTime += thisToolsTime
|
|
|
|
let nextToolChoice = toolChoice
|
|
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
|
|
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
|
|
|
|
if (remainingTools.length > 0) {
|
|
nextToolChoice = { tool: { name: remainingTools[0] } }
|
|
logger.info(`Forcing next tool: ${remainingTools[0]}`)
|
|
} else {
|
|
nextToolChoice = { auto: {} }
|
|
logger.info('All forced tools have been used, switching to auto')
|
|
}
|
|
} else if (hasUsedForcedTool && typeof originalToolChoice === 'object') {
|
|
nextToolChoice = { auto: {} }
|
|
logger.info('Switching to auto tool choice after forced tool was used')
|
|
}
|
|
|
|
const nextModelStartTime = Date.now()
|
|
|
|
const nextCommand = new ConverseCommand({
|
|
modelId: bedrockModelId,
|
|
messages: currentMessages,
|
|
system: systemPromptWithSchema.length > 0 ? systemPromptWithSchema : undefined,
|
|
inferenceConfig,
|
|
toolConfig: bedrockTools?.length
|
|
? { tools: bedrockTools, toolChoice: nextToolChoice }
|
|
: undefined,
|
|
})
|
|
|
|
currentResponse = await client.send(
|
|
nextCommand,
|
|
request.abortSignal ? { abortSignal: request.abortSignal } : undefined
|
|
)
|
|
|
|
const nextToolUseContentBlocks = (currentResponse.output?.message?.content || []).filter(
|
|
(block): block is ContentBlock & { toolUse: ToolUseBlock } => 'toolUse' in block
|
|
)
|
|
const nextToolUseBlocks = nextToolUseContentBlocks.map((block) => ({
|
|
name: block.toolUse.name || '',
|
|
}))
|
|
|
|
const nextCheckResult = checkForForcedToolUsage(
|
|
nextToolUseBlocks,
|
|
nextToolChoice,
|
|
forcedTools,
|
|
usedForcedTools
|
|
)
|
|
if (nextCheckResult) {
|
|
hasUsedForcedTool = nextCheckResult.hasUsedForcedTool
|
|
usedForcedTools = nextCheckResult.usedForcedTools
|
|
}
|
|
|
|
const nextModelEndTime = Date.now()
|
|
const thisModelTime = nextModelEndTime - nextModelStartTime
|
|
|
|
timeSegments.push({
|
|
type: 'model',
|
|
name: request.model,
|
|
startTime: nextModelStartTime,
|
|
endTime: nextModelEndTime,
|
|
duration: thisModelTime,
|
|
})
|
|
|
|
enrichLastModelSegmentFromBedrockResponse(timeSegments, currentResponse, {
|
|
model: request.model,
|
|
})
|
|
|
|
modelTime += thisModelTime
|
|
|
|
if (currentResponse.usage) {
|
|
tokens.input += currentResponse.usage.inputTokens || 0
|
|
tokens.output += currentResponse.usage.outputTokens || 0
|
|
tokens.total +=
|
|
(currentResponse.usage.inputTokens || 0) + (currentResponse.usage.outputTokens || 0)
|
|
|
|
const iterationCost = calculateCost(
|
|
request.model,
|
|
currentResponse.usage.inputTokens || 0,
|
|
currentResponse.usage.outputTokens || 0
|
|
)
|
|
cost.input += iterationCost.input
|
|
cost.output += iterationCost.output
|
|
cost.total += iterationCost.total
|
|
}
|
|
|
|
iterationCount++
|
|
}
|
|
|
|
if (structuredOutputTool && request.tools?.length) {
|
|
logger.info('Making final call with forced structured_output tool')
|
|
|
|
const structuredOutputStartTime = Date.now()
|
|
|
|
const structuredOutputCommand = new ConverseCommand({
|
|
modelId: bedrockModelId,
|
|
messages: currentMessages,
|
|
system: systemPromptWithSchema.length > 0 ? systemPromptWithSchema : undefined,
|
|
inferenceConfig,
|
|
toolConfig: {
|
|
tools: [structuredOutputTool],
|
|
toolChoice: { tool: { name: structuredOutputToolName } },
|
|
},
|
|
})
|
|
|
|
const structuredResponse = await client.send(
|
|
structuredOutputCommand,
|
|
request.abortSignal ? { abortSignal: request.abortSignal } : undefined
|
|
)
|
|
const structuredOutputEndTime = Date.now()
|
|
|
|
timeSegments.push({
|
|
type: 'model',
|
|
name: 'Structured output extraction',
|
|
startTime: structuredOutputStartTime,
|
|
endTime: structuredOutputEndTime,
|
|
duration: structuredOutputEndTime - structuredOutputStartTime,
|
|
})
|
|
|
|
enrichLastModelSegmentFromBedrockResponse(timeSegments, structuredResponse, {
|
|
model: request.model,
|
|
})
|
|
|
|
modelTime += structuredOutputEndTime - structuredOutputStartTime
|
|
|
|
const structuredOutputCall = structuredResponse.output?.message?.content?.find(
|
|
(block): block is ContentBlock & { toolUse: ToolUseBlock } =>
|
|
'toolUse' in block && block.toolUse?.name === structuredOutputToolName
|
|
)
|
|
|
|
if (structuredOutputCall) {
|
|
content = JSON.stringify(structuredOutputCall.toolUse.input, null, 2)
|
|
hasExtractedStructuredOutput = true
|
|
logger.info('Extracted structured output from forced tool call')
|
|
} else {
|
|
logger.warn('Structured output tool was forced but no tool call found in response')
|
|
}
|
|
|
|
if (structuredResponse.usage) {
|
|
tokens.input += structuredResponse.usage.inputTokens || 0
|
|
tokens.output += structuredResponse.usage.outputTokens || 0
|
|
tokens.total +=
|
|
(structuredResponse.usage.inputTokens || 0) +
|
|
(structuredResponse.usage.outputTokens || 0)
|
|
|
|
const structuredCost = calculateCost(
|
|
request.model,
|
|
structuredResponse.usage.inputTokens || 0,
|
|
structuredResponse.usage.outputTokens || 0
|
|
)
|
|
cost.input += structuredCost.input
|
|
cost.output += structuredCost.output
|
|
cost.total += structuredCost.total
|
|
}
|
|
}
|
|
|
|
const providerEndTime = Date.now()
|
|
const providerEndTimeISO = new Date(providerEndTime).toISOString()
|
|
const totalDuration = providerEndTime - providerStartTime
|
|
|
|
if (request.stream && !shouldStreamToolCalls && !hasExtractedStructuredOutput) {
|
|
logger.info('Using streaming for final Bedrock response after tool processing')
|
|
|
|
const messagesHaveToolContent = currentMessages.some((msg) =>
|
|
msg.content?.some(
|
|
(block) =>
|
|
('toolUse' in block && block.toolUse) || ('toolResult' in block && block.toolResult)
|
|
)
|
|
)
|
|
|
|
const streamToolConfig: ToolConfiguration | undefined =
|
|
messagesHaveToolContent && request.tools?.length
|
|
? {
|
|
tools: request.tools.map((tool) => ({
|
|
toolSpec: {
|
|
name: tool.id,
|
|
description: tool.description,
|
|
inputSchema: {
|
|
json: {
|
|
type: 'object',
|
|
properties: tool.parameters.properties,
|
|
required: tool.parameters.required,
|
|
},
|
|
},
|
|
},
|
|
})),
|
|
toolChoice: { auto: {} },
|
|
}
|
|
: undefined
|
|
|
|
const streamCommand = new ConverseStreamCommand({
|
|
modelId: bedrockModelId,
|
|
messages: currentMessages,
|
|
system: systemPromptWithSchema.length > 0 ? systemPromptWithSchema : undefined,
|
|
inferenceConfig,
|
|
toolConfig: streamToolConfig,
|
|
})
|
|
|
|
const streamResponse = await client.send(
|
|
streamCommand,
|
|
request.abortSignal ? { abortSignal: request.abortSignal } : undefined
|
|
)
|
|
|
|
if (!streamResponse.stream) {
|
|
throw new Error('No stream returned from Bedrock')
|
|
}
|
|
|
|
const bedrockStream = streamResponse.stream
|
|
const streamingResult = createStreamingExecution({
|
|
model: request.model,
|
|
providerStartTime,
|
|
providerStartTimeISO,
|
|
timing: {
|
|
kind: 'accumulated',
|
|
modelTime,
|
|
toolsTime,
|
|
firstResponseTime,
|
|
iterations: iterationCount + 1,
|
|
timeSegments,
|
|
},
|
|
initialTokens: { input: tokens.input, output: tokens.output, total: tokens.total },
|
|
initialCost: {
|
|
input: cost.input,
|
|
output: cost.output,
|
|
toolCost: undefined as number | undefined,
|
|
total: cost.total,
|
|
},
|
|
toolCalls:
|
|
toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined,
|
|
isStreaming: true,
|
|
createStream: ({ output, finalizeTiming }) =>
|
|
createReadableStreamFromBedrockStream(bedrockStream, (streamContent, usage) => {
|
|
output.content = streamContent
|
|
output.tokens = {
|
|
input: tokens.input + usage.inputTokens,
|
|
output: tokens.output + usage.outputTokens,
|
|
total: tokens.total + usage.inputTokens + usage.outputTokens,
|
|
}
|
|
|
|
const streamCost = calculateCost(request.model, usage.inputTokens, usage.outputTokens)
|
|
const tc = sumToolCosts(toolResults)
|
|
output.cost = {
|
|
input: cost.input + streamCost.input,
|
|
output: cost.output + streamCost.output,
|
|
toolCost: tc || undefined,
|
|
total: cost.total + streamCost.total + tc,
|
|
}
|
|
|
|
finalizeTiming()
|
|
}),
|
|
})
|
|
|
|
return streamingResult
|
|
}
|
|
|
|
return {
|
|
content,
|
|
model: request.model,
|
|
tokens,
|
|
cost: {
|
|
input: cost.input,
|
|
output: cost.output,
|
|
total: cost.total,
|
|
pricing: cost.pricing,
|
|
},
|
|
toolCalls:
|
|
toolCalls.length > 0
|
|
? toolCalls.map((tc) => ({
|
|
name: tc.name,
|
|
arguments: tc.arguments as Record<string, any>,
|
|
startTime: tc.startTime,
|
|
endTime: tc.endTime,
|
|
duration: tc.duration,
|
|
result: tc.result,
|
|
}))
|
|
: undefined,
|
|
toolResults: toolResults.length > 0 ? toolResults : undefined,
|
|
timing: {
|
|
startTime: providerStartTimeISO,
|
|
endTime: providerEndTimeISO,
|
|
duration: totalDuration,
|
|
modelTime,
|
|
toolsTime,
|
|
firstResponseTime,
|
|
iterations: iterationCount + 1,
|
|
timeSegments,
|
|
},
|
|
}
|
|
} catch (error) {
|
|
const providerEndTime = Date.now()
|
|
const providerEndTimeISO = new Date(providerEndTime).toISOString()
|
|
const totalDuration = providerEndTime - providerStartTime
|
|
|
|
logger.error('Error in Bedrock request:', {
|
|
error,
|
|
duration: totalDuration,
|
|
})
|
|
|
|
throw new ProviderError(toError(error).message, {
|
|
startTime: providerStartTimeISO,
|
|
endTime: providerEndTimeISO,
|
|
duration: totalDuration,
|
|
})
|
|
}
|
|
},
|
|
}
|