-
Notifications
You must be signed in to change notification settings - Fork 201
AWS Bedrock provider support #542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
inercia
commented
Oct 17, 2025
- AWS Bedrock provider, supporting multiple model families (Anthropic Claude, Amazon Titan, Meta Llama, and Mistral).
d2e7d92 to
93b0ac6
Compare
780d7fd to
f5a38ac
Compare
| // processClaudeChunk processes Claude model chunks | ||
| func (a *StreamAdapter) processClaudeChunk(chunk []byte) (chat.MessageStreamResponse, error) { | ||
| // Claude chunks follow the Messages API streaming format | ||
| var claudeChunk struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more of a question in general about the code, I see there's a lot of hand-made code for calling different models, doesn't AWS provide an SDK that can help with these? I'm not really a pro in aws and their sdks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. This was based on some previous code of mine, but I have updated the code for using more modern features of the AWS Bedrock SDK.
|
@inercia small update, sorry this is taking this long, I'm trying to get access to Bedrock to try this out. Had a small question, see above. This is a lot of code, can we ping you if we have some issues in the future with this part of the code? :) |
f5a38ac to
24e64ba
Compare
|
Thanks for reviewing this. Just for the record, I have been testing this with the following code: #!/bin/bash
# get the AWS Bedrock creds
set -a
source .env.bedrock
export OPENAI_API_KEY="empty"
export ANTHROPIC_API_KEY="empty"
echo "************************************************"
./bin/cagent run --model="amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0" \
--debug --tui=false --yolo \
./examples/haiku.yaml \
"Write a haiku about the weather"
echo "************************************************"
./bin/cagent run --model="amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0" \
--debug --tui=false --yolo \
./examples/script_shell.yaml \
"Show me all the GitHub repositories for GitHub user 'inercia'"
echo "************************************************"
./bin/cagent run --model="amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0" \
--debug --tui=false --yolo \
./examples/pythoninst.yaml \
"Create a Python program for calculating the Fibonacci sequence. Find the optimal algorithm for the task. Check the code works by running it"
|
24e64ba to
ff4d630
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive AWS Bedrock provider support to cagent, enabling access to various AI models hosted on AWS Bedrock including Anthropic Claude, Amazon Titan, Meta Llama, and Mistral. The implementation follows the established provider pattern in the codebase and includes support for both AWS credential chain and bearer token authentication methods.
Key changes:
- New Bedrock provider implementation using AWS SDK v2 with Converse API for streaming
- Flexible authentication supporting AWS credentials and custom bearer tokens (for proxy scenarios)
- Region configuration via environment variables or explicit provider options
- Comprehensive documentation and example configuration files
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/model/provider/provider.go | Registers the new "amazon-bedrock" provider in the factory function |
| pkg/model/provider/bedrock/client.go | Core Bedrock client implementation with authentication, region handling, message/tool conversion |
| pkg/model/provider/bedrock/adapter.go | Stream adapter to convert Bedrock Converse API events to cagent's MessageStream interface |
| pkg/model/provider/bedrock/client_test.go | Unit tests for client initialization, configuration, and region handling |
| docs/USAGE.md | Documentation for authentication methods, configuration options, and usage examples |
| examples/bedrock.yaml | Example configuration demonstrating Bedrock provider usage |
| go.mod, go.sum | AWS SDK v2 dependencies for Bedrock Runtime API |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ID: *start.Value.ToolUseId, | ||
| Type: "function", | ||
| Function: tools.FunctionCall{ | ||
| Name: *start.Value.Name, | ||
| }, | ||
| } | ||
| response.Choices[0].Delta.ToolCalls = []tools.ToolCall{toolCall} | ||
|
|
||
| // Store tool call info for delta events | ||
| if e.Value.ContentBlockIndex != nil { | ||
| a.toolCallData[int(*e.Value.ContentBlockIndex)] = &toolCallInfo{ | ||
| ID: *start.Value.ToolUseId, | ||
| Name: *start.Value.Name, |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential nil pointer dereference. The code dereferences start.Value.ToolUseId and start.Value.Name without checking if they are nil first. While these values should typically be present in the AWS SDK response, defensive nil checks should be added to prevent potential panics if the API returns unexpected data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added defensive nil checks for start.Value.ToolUseId and start.Value.Name before dereferencing them, with appropriate warning logging.
| @@ -0,0 +1,143 @@ | |||
| package bedrock | |||
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The StreamAdapter implementation in adapter.go lacks test coverage. The adapter handles critical streaming logic including tool calls, content deltas, and error handling. Similar providers like Gemini have adapter_test.go files that test the stream processing logic. Consider adding tests for the StreamAdapter to cover scenarios like tool call streaming, text streaming, error handling, and finish reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The adapter tests would require mocking the AWS Bedrock stream events which is complex. I'd recommend addressing this in a follow-up PR if we want to proceed with it.
284d323 to
a3006e7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // processConverseEvent processes a Converse API stream event | ||
| func (a *StreamAdapter) processConverseEvent(event types.ConverseStreamOutput) (chat.MessageStreamResponse, error) { | ||
| response := chat.MessageStreamResponse{ | ||
| Model: a.model, | ||
| Choices: []chat.MessageStreamChoice{ | ||
| { | ||
| Index: 0, | ||
| Delta: chat.MessageDelta{ | ||
| Role: string(chat.MessageRoleAssistant), | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| switch e := event.(type) { | ||
| case *types.ConverseStreamOutputMemberMessageStart: | ||
| // Message start event - provides role | ||
| slog.Debug("Converse MessageStart event", "role", e.Value.Role) | ||
| return response, nil | ||
|
|
||
| case *types.ConverseStreamOutputMemberContentBlockStart: | ||
| // Content block start - may be text or tool use | ||
| if e.Value.Start != nil { | ||
| switch start := e.Value.Start.(type) { | ||
| case *types.ContentBlockStartMemberToolUse: | ||
| // Tool use started - check for nil values before dereferencing | ||
| if start.Value.ToolUseId == nil || start.Value.Name == nil { | ||
| slog.Warn("Converse ContentBlockStart (ToolUse) missing required fields", | ||
| "tool_use_id_nil", start.Value.ToolUseId == nil, | ||
| "name_nil", start.Value.Name == nil) | ||
| return response, nil | ||
| } | ||
|
|
||
| toolCall := tools.ToolCall{ | ||
| ID: *start.Value.ToolUseId, | ||
| Type: "function", | ||
| Function: tools.FunctionCall{ | ||
| Name: *start.Value.Name, | ||
| }, | ||
| } | ||
| response.Choices[0].Delta.ToolCalls = []tools.ToolCall{toolCall} | ||
|
|
||
| // Store tool call info for delta events | ||
| if e.Value.ContentBlockIndex != nil { | ||
| a.toolCallData[int(*e.Value.ContentBlockIndex)] = &toolCallInfo{ | ||
| ID: *start.Value.ToolUseId, | ||
| Name: *start.Value.Name, | ||
| } | ||
| } | ||
|
|
||
| slog.Debug("Converse ContentBlockStart (ToolUse)", | ||
| "tool_id", *start.Value.ToolUseId, | ||
| "tool_name", *start.Value.Name, | ||
| "index", e.Value.ContentBlockIndex) | ||
| } | ||
| } | ||
| return response, nil | ||
|
|
||
| case *types.ConverseStreamOutputMemberContentBlockDelta: | ||
| // Content block delta - streaming content | ||
| if e.Value.Delta != nil { | ||
| switch delta := e.Value.Delta.(type) { | ||
| case *types.ContentBlockDeltaMemberText: | ||
| // Text content delta | ||
| response.Choices[0].Delta.Content = delta.Value | ||
| slog.Debug("Converse ContentBlockDelta (Text)", "length", len(delta.Value)) | ||
|
|
||
| case *types.ContentBlockDeltaMemberToolUse: | ||
| // Tool use input delta (streaming JSON arguments) | ||
| // Accumulate but DON'T send to runtime until complete | ||
| if e.Value.ContentBlockIndex != nil && delta.Value.Input != nil { | ||
| if toolInfo, ok := a.toolCallData[int(*e.Value.ContentBlockIndex)]; ok { | ||
| toolInfo.Arguments += *delta.Value.Input | ||
| slog.Debug("Converse ContentBlockDelta (ToolUse) accumulated", | ||
| "chunk_length", len(*delta.Value.Input), | ||
| "total_length", len(toolInfo.Arguments)) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return response, nil | ||
|
|
||
| case *types.ConverseStreamOutputMemberContentBlockStop: | ||
| // Content block stopped - now send complete tool call if this was a tool use block | ||
| if e.Value.ContentBlockIndex != nil { | ||
| if toolInfo, ok := a.toolCallData[int(*e.Value.ContentBlockIndex)]; ok { | ||
| // Ensure arguments is valid JSON - if empty, use empty object | ||
| args := toolInfo.Arguments | ||
| if args == "" { | ||
| args = "{}" | ||
| } | ||
|
|
||
| // Send the complete tool call now | ||
| toolCall := tools.ToolCall{ | ||
| ID: toolInfo.ID, | ||
| Type: "function", | ||
| Function: tools.FunctionCall{ | ||
| Name: toolInfo.Name, | ||
| Arguments: args, | ||
| }, | ||
| } | ||
| response.Choices[0].Delta.ToolCalls = []tools.ToolCall{toolCall} | ||
| slog.Debug("Converse ContentBlockStop - sending complete tool call", | ||
| "tool_id", toolInfo.ID, | ||
| "tool_name", toolInfo.Name, | ||
| "args_length", len(args)) | ||
| } | ||
| } | ||
| slog.Debug("Converse ContentBlockStop", "index", e.Value.ContentBlockIndex) | ||
| return response, nil | ||
|
|
||
| case *types.ConverseStreamOutputMemberMessageStop: | ||
| // Message stopped - provides stop reason | ||
| if e.Value.StopReason != "" { | ||
| response.Choices[0].FinishReason = mapConverseStopReason(e.Value.StopReason) | ||
| slog.Debug("Converse MessageStop", "stop_reason", e.Value.StopReason) | ||
| } | ||
| return response, nil | ||
|
|
||
| case *types.ConverseStreamOutputMemberMetadata: | ||
| // Metadata event - provides token usage | ||
| if e.Value.Usage != nil { | ||
| usage := &chat.Usage{} | ||
| if e.Value.Usage.InputTokens != nil { | ||
| usage.InputTokens = int64(*e.Value.Usage.InputTokens) | ||
| } | ||
| if e.Value.Usage.OutputTokens != nil { | ||
| usage.OutputTokens = int64(*e.Value.Usage.OutputTokens) | ||
| } | ||
| response.Usage = usage | ||
| slog.Debug("Converse Metadata", "input_tokens", usage.InputTokens, "output_tokens", usage.OutputTokens) | ||
| } | ||
| return response, nil | ||
|
|
||
| default: | ||
| slog.Warn("Unexpected Converse stream event", "type", fmt.Sprintf("%T", event)) | ||
| return chat.MessageStreamResponse{}, fmt.Errorf("unexpected stream event: %T", event) | ||
| } | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The processConverseEvent function and the StreamAdapter lack test coverage. This is a complex event processing function that handles multiple event types, tool calls, text streaming, and usage tracking. Consider adding unit tests for each event type (MessageStart, ContentBlockStart, ContentBlockDelta, ContentBlockStop, MessageStop, Metadata) and verify tool call accumulation logic works correctly.
| // convertToConverseMessages converts cagent messages to Converse API format | ||
| func convertToConverseMessages(messages []chat.Message) ([]types.Message, []types.SystemContentBlock, error) { | ||
| var converseMessages []types.Message | ||
| var systemBlocks []types.SystemContentBlock | ||
|
|
||
| for i := 0; i < len(messages); i++ { | ||
| msg := messages[i] | ||
|
|
||
| // System messages are handled separately | ||
| if msg.Role == chat.MessageRoleSystem { | ||
| systemBlocks = append(systemBlocks, &types.SystemContentBlockMemberText{ | ||
| Value: msg.Content, | ||
| }) | ||
| continue | ||
| } | ||
|
|
||
| // Convert role | ||
| var role types.ConversationRole | ||
| switch msg.Role { | ||
| case chat.MessageRoleUser: | ||
| role = types.ConversationRoleUser | ||
| case chat.MessageRoleAssistant: | ||
| role = types.ConversationRoleAssistant | ||
| case chat.MessageRoleTool: | ||
| // Tool results are sent as user messages with tool result blocks | ||
| role = types.ConversationRoleUser | ||
| default: | ||
| return nil, nil, fmt.Errorf("unsupported message role: %s", msg.Role) | ||
| } | ||
|
|
||
| // Build content blocks | ||
| var contentBlocks []types.ContentBlock | ||
|
|
||
| // Handle tool results - group consecutive tool results into one user message | ||
| if msg.Role == chat.MessageRoleTool && msg.ToolCallID != "" { | ||
| // Collect all consecutive tool results | ||
| toolResults := []chat.Message{msg} | ||
| j := i + 1 | ||
| for j < len(messages) && messages[j].Role == chat.MessageRoleTool { | ||
| toolResults = append(toolResults, messages[j]) | ||
| j++ | ||
| } | ||
|
|
||
| // Convert all tool results into content blocks | ||
| for _, tr := range toolResults { | ||
| var toolResultContent []types.ToolResultContentBlock | ||
| toolResultContent = append(toolResultContent, &types.ToolResultContentBlockMemberText{ | ||
| Value: tr.Content, | ||
| }) | ||
|
|
||
| contentBlocks = append(contentBlocks, &types.ContentBlockMemberToolResult{ | ||
| Value: types.ToolResultBlock{ | ||
| ToolUseId: aws.String(tr.ToolCallID), | ||
| Content: toolResultContent, | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| // Skip the tool results we already processed | ||
| i = j - 1 | ||
| } else if msg.Role == chat.MessageRoleAssistant && len(msg.ToolCalls) > 0 { | ||
| // Assistant message with tool calls | ||
| if msg.Content != "" { | ||
| contentBlocks = append(contentBlocks, &types.ContentBlockMemberText{ | ||
| Value: msg.Content, | ||
| }) | ||
| } | ||
|
|
||
| // Add tool use blocks | ||
| for _, tc := range msg.ToolCalls { | ||
| // Parse tool arguments | ||
| var input map[string]any | ||
| if tc.Function.Arguments != "" { | ||
| if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil { | ||
| slog.Warn("Failed to unmarshal tool arguments", "tool", tc.Function.Name, "error", err) | ||
| input = make(map[string]any) | ||
| } | ||
| } else { | ||
| input = make(map[string]any) | ||
| } | ||
|
|
||
| // Convert to document type | ||
| inputDoc, err := convertToDocument(input) | ||
| if err != nil { | ||
| return nil, nil, fmt.Errorf("failed to convert tool input: %w", err) | ||
| } | ||
|
|
||
| contentBlocks = append(contentBlocks, &types.ContentBlockMemberToolUse{ | ||
| Value: types.ToolUseBlock{ | ||
| ToolUseId: aws.String(tc.ID), | ||
| Name: aws.String(tc.Function.Name), | ||
| Input: inputDoc, | ||
| }, | ||
| }) | ||
| } | ||
| } else { | ||
| // Regular text message | ||
| if msg.Content != "" { | ||
| contentBlocks = append(contentBlocks, &types.ContentBlockMemberText{ | ||
| Value: msg.Content, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| if len(contentBlocks) > 0 { | ||
| converseMessages = append(converseMessages, types.Message{ | ||
| Role: role, | ||
| Content: contentBlocks, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| return converseMessages, systemBlocks, nil | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The convertToConverseMessages function lacks test coverage. This is a critical function that handles message conversion including system messages, tool calls, and tool results. Consider adding unit tests to verify correct handling of different message types, edge cases like empty tool results, consecutive tool messages, and invalid message roles. Other providers like Anthropic have extensive test coverage for their message conversion functions (see pkg/model/provider/anthropic/client_test.go).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a valid suggestion, but adding comprehensive unit tests for this function is a larger effort that would be better addressed in a follow-up PR. The function is already exercised through integration paths.
| // convertToConverseTools converts cagent tools to Converse API format | ||
| func convertToConverseTools(tools []cagentTools.Tool) ([]types.Tool, error) { | ||
| var converseTools []types.Tool | ||
|
|
||
| for _, tool := range tools { | ||
| // Convert tool parameters schema | ||
| schemaMap, err := cagentTools.SchemaToMap(tool.Parameters) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to convert tool %s schema: %w", tool.Name, err) | ||
| } | ||
|
|
||
| // Convert schema to document | ||
| inputSchema, err := convertToDocument(schemaMap) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to convert tool %s input schema: %w", tool.Name, err) | ||
| } | ||
|
|
||
| converseTools = append(converseTools, &types.ToolMemberToolSpec{ | ||
| Value: types.ToolSpecification{ | ||
| Name: aws.String(tool.Name), | ||
| Description: aws.String(tool.Description), | ||
| InputSchema: &types.ToolInputSchemaMemberJson{ | ||
| Value: inputSchema, | ||
| }, | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| return converseTools, nil | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The convertToConverseTools function lacks test coverage. This function converts tool schemas to the Bedrock Converse API format. Consider adding tests to verify correct schema conversion, handling of tool descriptions, and proper error handling when schema conversion fails. This is especially important since different model families on Bedrock might have different schema requirements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above - valid but would be better as follow-up work.
a3006e7 to
28474dd
Compare
| } | ||
| return response, nil | ||
|
|
||
| case *types.ConverseStreamOutputMemberMetadata: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't gone deep into the code yet, but testing this branch with global.anthropic.claude-haiku-4-5-20251001-v1:0 I don't see any token usage info in the TUI. Could you double check that?
3a4ec8d to
24801ef
Compare
- AWS Bedrock provider, supporting multiple model families (Anthropic Claude, Amazon Titan, Meta Llama, and Mistral). Signed-off-by: Alvaro Saurin <[email protected]>
Signed-off-by: Alvaro Saurin <[email protected]>
24801ef to
4554e6b
Compare