Skip to content

Commit f577ed3

Browse files
committed
testing bedrock
1 parent b6a11d0 commit f577ed3

9 files changed

Lines changed: 2818 additions & 25 deletions

File tree

go.mod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ require (
1414
dario.cat/mergo v1.0.0 // indirect
1515
github.com/Microsoft/go-winio v0.6.2 // indirect
1616
github.com/ProtonMail/go-crypto v1.1.6 // indirect
17+
github.com/aws/aws-sdk-go-v2 v1.41.2 // indirect
18+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
19+
github.com/aws/aws-sdk-go-v2/config v1.32.10 // indirect
20+
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect
21+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
22+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
23+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
24+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
25+
github.com/aws/aws-sdk-go-v2/service/bedrock v1.55.0 // indirect
26+
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0 // indirect
27+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
28+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
29+
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
30+
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
31+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
32+
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
33+
github.com/aws/smithy-go v1.24.1 // indirect
1734
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
1835
github.com/charmbracelet/bubbles v1.0.0 // indirect
1936
github.com/charmbracelet/colorprofile v0.4.1 // indirect

go.sum

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,40 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
5252
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
5353
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
5454
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
55+
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
56+
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
57+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
58+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
59+
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
60+
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
61+
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
62+
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
63+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
64+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
65+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
66+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
67+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
68+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
69+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
70+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
71+
github.com/aws/aws-sdk-go-v2/service/bedrock v1.55.0 h1:ZGDv11tfxsY0sA6juMAarqzH94TLSpmUA8n3soch7BY=
72+
github.com/aws/aws-sdk-go-v2/service/bedrock v1.55.0/go.mod h1:NzNjovvosgFkbF8zPLhYDITchCMZapzCIClB+6kpvQw=
73+
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0 h1:TDKR8ACRw7G+GFaQlhoy6biu+8q6ZtSddQCy9avMdMI=
74+
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0/go.mod h1:XlhOh5Ax/lesqN4aZCUgj9vVJed5VoXYHHFYGAlJEwU=
75+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
76+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
77+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
78+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
79+
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
80+
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
81+
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
82+
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
83+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
84+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
85+
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
86+
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
87+
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
88+
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
5589
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
5690
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
5791
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=

internal/bedrock/client.go

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
package bedrock
2+
3+
import (
4+
stdcontext "context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
12+
"github.com/aws/aws-sdk-go-v2/config"
13+
"github.com/aws/aws-sdk-go-v2/credentials"
14+
awsbedrock "github.com/aws/aws-sdk-go-v2/service/bedrock"
15+
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
16+
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
17+
)
18+
19+
// BedrockClient handles communication with AWS Bedrock API
20+
type BedrockClient struct {
21+
client *bedrockruntime.Client
22+
bedrockClient *awsbedrock.Client
23+
region string
24+
}
25+
26+
// anthropicRequest represents the request body for Claude models on AWS Bedrock
27+
type anthropicRequest struct {
28+
AnthropicVersion string `json:"anthropic_version"`
29+
MaxTokens int `json:"max_tokens"`
30+
Messages []anthropicMessage `json:"messages"`
31+
}
32+
33+
// anthropicMessage represents a message in the Anthropic Messages API format
34+
type anthropicMessage struct {
35+
Role string `json:"role"`
36+
Content string `json:"content"`
37+
}
38+
39+
// NewBedrockClient creates a new Bedrock client
40+
// Args:
41+
//
42+
// apiKey: string - AWS credentials in format "ACCESS_KEY_ID:SECRET_ACCESS_KEY"
43+
// region: string - AWS region (default: "us-east-1")
44+
//
45+
// Returns: initialized BedrockClient, error if apiKey is empty or invalid format
46+
func NewBedrockClient(apiKey, region string) (*BedrockClient, error) {
47+
if apiKey == "" {
48+
return nil, fmt.Errorf("Bedrock API key not provided")
49+
}
50+
51+
// Parse API key in format "ACCESS_KEY_ID:SECRET_ACCESS_KEY"
52+
parts := splitAPIKey(apiKey)
53+
if len(parts) != 2 {
54+
return nil, fmt.Errorf("invalid API key format: expected ACCESS_KEY_ID:SECRET_ACCESS_KEY")
55+
}
56+
57+
accessKeyID := parts[0]
58+
secretAccessKey := parts[1]
59+
60+
if accessKeyID == "" || secretAccessKey == "" {
61+
return nil, fmt.Errorf("invalid API key format: both access key ID and secret access key are required")
62+
}
63+
64+
if region == "" {
65+
region = "us-east-1"
66+
}
67+
68+
// Create AWS config with static credentials
69+
cfg, err := config.LoadDefaultConfig(stdcontext.TODO(),
70+
config.WithRegion(region),
71+
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
72+
accessKeyID,
73+
secretAccessKey,
74+
"", // Session token (not used)
75+
)),
76+
)
77+
if err != nil {
78+
return nil, fmt.Errorf("failed to load AWS config: %w", err)
79+
}
80+
81+
// Create Bedrock Runtime client
82+
client := bedrockruntime.NewFromConfig(cfg)
83+
84+
// Create Bedrock client for control plane operations (like ListFoundationModels)
85+
bedrockClient := awsbedrock.NewFromConfig(cfg)
86+
87+
return &BedrockClient{
88+
client: client,
89+
bedrockClient: bedrockClient,
90+
region: region,
91+
}, nil
92+
}
93+
94+
// splitAPIKey splits the API key by colon separator
95+
func splitAPIKey(apiKey string) []string {
96+
result := []string{}
97+
current := ""
98+
99+
for _, char := range apiKey {
100+
if char == ':' {
101+
result = append(result, current)
102+
current = ""
103+
} else {
104+
current += string(char)
105+
}
106+
}
107+
108+
if current != "" || len(result) > 0 {
109+
result = append(result, current)
110+
}
111+
112+
return result
113+
}
114+
115+
// formatAWSError formats AWS SDK errors with HTTP status codes and descriptive messages
116+
func formatAWSError(err error, operation string) error {
117+
if err == nil {
118+
return nil
119+
}
120+
121+
// Try to extract HTTP response error
122+
var httpErr *awshttp.ResponseError
123+
if errors.As(err, &httpErr) {
124+
statusCode := httpErr.Response.StatusCode
125+
126+
// Format error message based on status code
127+
var message string
128+
switch statusCode {
129+
case http.StatusBadRequest: // 400
130+
message = "Bad Request - Invalid request format or parameters"
131+
case http.StatusForbidden: // 403
132+
message = "Invalid credentials or insufficient permissions"
133+
case http.StatusNotFound: // 404
134+
message = "Model may be invalid or unavailable in region"
135+
case http.StatusTooManyRequests: // 429
136+
message = "Rate limit exceeded - please retry after a delay"
137+
case http.StatusInternalServerError: // 500
138+
message = "AWS Bedrock service error"
139+
default:
140+
message = fmt.Sprintf("HTTP error: %s", httpErr.Error())
141+
}
142+
143+
return fmt.Errorf("Bedrock %s (status %d): %s", operation, statusCode, message)
144+
}
145+
146+
// If not an HTTP error, return the original error with context
147+
return fmt.Errorf("Bedrock %s: %w", operation, err)
148+
}
149+
150+
// buildRequestBody constructs the request body for Claude models on AWS Bedrock
151+
// Args:
152+
//
153+
// prompt: string - user's prompt to the AI
154+
//
155+
// Returns: anthropicRequest struct ready to be marshaled to JSON
156+
func buildRequestBody(prompt string) anthropicRequest {
157+
return anthropicRequest{
158+
AnthropicVersion: "bedrock-2023-05-31",
159+
MaxTokens: 4096,
160+
Messages: []anthropicMessage{
161+
{
162+
Role: "user",
163+
Content: prompt,
164+
},
165+
},
166+
}
167+
}
168+
169+
// IsAvailable checks if Bedrock API is available
170+
// Returns: true if Bedrock is reachable, error if not
171+
func (bc *BedrockClient) IsAvailable() (bool, error) {
172+
// The API key validation is already done in NewBedrockClient
173+
// Here we attempt a lightweight API call to verify connectivity
174+
175+
// Use ListFoundationModels as a lightweight check
176+
// This verifies both connectivity and credentials
177+
ctx := stdcontext.TODO()
178+
_, err := bc.bedrockClient.ListFoundationModels(ctx, &awsbedrock.ListFoundationModelsInput{})
179+
180+
if err != nil {
181+
return false, formatAWSError(err, "IsAvailable")
182+
}
183+
184+
return true, nil
185+
}
186+
187+
// Generate generates AI response with streaming
188+
// Args:
189+
//
190+
// prompt: string - user's prompt to the AI
191+
// model: string - model name to use (default: "anthropic.claude-haiku-4-5-v1:0")
192+
// context: []int - optional context tokens from previous conversation
193+
//
194+
// Returns: channel for streaming response chunks, error if request fails
195+
func (bc *BedrockClient) Generate(prompt string, model string, context []int) (<-chan string, error) {
196+
// Default model to Claude Haiku 4.5 if empty
197+
if model == "" {
198+
model = "anthropic.claude-haiku-4-5-v1:0"
199+
}
200+
201+
// Construct request body using anthropicRequest struct
202+
requestBody := buildRequestBody(prompt)
203+
204+
// Marshal request body to JSON
205+
requestBodyJSON, err := json.Marshal(requestBody)
206+
if err != nil {
207+
return nil, fmt.Errorf("failed to marshal request body: %w", err)
208+
}
209+
210+
// Call InvokeModelWithResponseStream API
211+
ctx := stdcontext.Background()
212+
output, err := bc.client.InvokeModelWithResponseStream(ctx, &bedrockruntime.InvokeModelWithResponseStreamInput{
213+
ModelId: &model,
214+
Body: requestBodyJSON,
215+
ContentType: aws.String("application/json"),
216+
})
217+
if err != nil {
218+
return nil, formatAWSError(err, "Generate")
219+
}
220+
221+
// Create buffered channel (capacity 10) for response chunks
222+
responseChan := make(chan string, 10)
223+
224+
// Launch goroutine to process event stream
225+
go func() {
226+
defer close(responseChan)
227+
defer output.GetStream().Close()
228+
229+
// Track if we encountered an error during processing
230+
var processingErr error
231+
232+
// Process event stream
233+
for event := range output.GetStream().Events() {
234+
switch e := event.(type) {
235+
case *types.ResponseStreamMemberChunk:
236+
// Parse the chunk bytes to extract text content
237+
var response map[string]any
238+
if err := json.Unmarshal(e.Value.Bytes, &response); err != nil {
239+
// Event parsing failure - send error and stop processing
240+
processingErr = fmt.Errorf("error parsing response: %w", err)
241+
responseChan <- fmt.Sprintf("Error parsing response: %v", err)
242+
return
243+
}
244+
245+
// Check for content_block_delta event type
246+
if eventType, ok := response["type"].(string); ok && eventType == "content_block_delta" {
247+
// Extract delta object
248+
if delta, ok := response["delta"].(map[string]any); ok {
249+
// Extract text from delta
250+
if text, ok := delta["text"].(string); ok && text != "" {
251+
responseChan <- text
252+
}
253+
}
254+
}
255+
256+
// Check for message_stop event to close channel
257+
if eventType, ok := response["type"].(string); ok && eventType == "message_stop" {
258+
return
259+
}
260+
261+
default:
262+
// Ignore other event types (including error events which are handled by stream.Err())
263+
}
264+
}
265+
266+
// Check for stream errors after processing completes
267+
// This catches errors that occurred during streaming but weren't event parsing errors
268+
if processingErr == nil {
269+
if err := output.GetStream().Err(); err != nil {
270+
formattedErr := formatAWSError(err, "streaming")
271+
responseChan <- fmt.Sprintf("Stream error: %v", formattedErr)
272+
}
273+
}
274+
}()
275+
276+
// Return channel immediately
277+
return responseChan, nil
278+
}
279+
280+
// ListModels lists available Bedrock models
281+
// Returns: slice of model names, error if request fails
282+
func (bc *BedrockClient) ListModels() ([]string, error) {
283+
// Return hardcoded list of supported Claude models
284+
return []string{
285+
"anthropic.claude-haiku-4-5-v1:0",
286+
"anthropic.claude-3-5-sonnet-20241022-v2:0",
287+
"anthropic.claude-3-5-haiku-20241022-v1:0",
288+
}, nil
289+
}

0 commit comments

Comments
 (0)