bedrock/README.md 🔗
@@ -0,0 +1,3 @@
+# Bedrock
+
+* Create an API key [on this page](https://eu-north-1.console.aws.amazon.com/bedrock/home#/api-keys/long-term/create).
Andrey Nering created
bedrock/README.md | 3
bedrock/bedrock.go | 125 +++++++++++++++++++++++++++++++++++++
bedrock/claude.go | 54 +++++++++++++++
bedrock/language_model.go | 85 +++++++++++++++++++++++++
bedrock/misc.go | 18 +++++
bedrock/provider_options.go | 15 ++++
go.mod | 16 ++++
go.sum | 32 +++++++++
providertests/bedrock_test.go | 47 +++++++++++++
9 files changed, 395 insertions(+)
@@ -0,0 +1,3 @@
+# Bedrock
+
+* Create an API key [on this page](https://eu-north-1.console.aws.amazon.com/bedrock/home#/api-keys/long-term/create).
@@ -0,0 +1,125 @@
+package bedrock
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
+ "github.com/charmbracelet/fantasy/ai"
+)
+
+const (
+ Name = "bedrock"
+ // DefaultURL = "https://bedrock-runtime.amazonaws.com"
+)
+
+type options struct {
+ name string
+ httpClient bedrockruntime.HTTPClient
+
+ // region string
+ // accessKey string
+ // secretKey string
+ // sessionToken string
+ // endpoint string
+}
+
+type provider struct {
+ options options
+ client *bedrockruntime.Client
+}
+
+type Option = func(*options)
+
+func New(ctx context.Context, opts ...Option) (ai.Provider, error) {
+ providerOptions := options{
+ name: Name,
+ }
+ for _, o := range opts {
+ o(&providerOptions)
+ }
+
+ cfg, err := config.LoadDefaultConfig(ctx) //, config.WithRegion(providerOptions.region))
+ if err != nil {
+ return nil, fmt.Errorf("fantasy: unable to load default aws config: %w", err)
+ }
+
+ // if providerOptions.accessKey != "" && providerOptions.secretKey != "" {
+ // cfg.Credentials = aws.CredentialsProviderFunc(
+ // func(ctx context.Context) (aws.Credentials, error) {
+ // return aws.Credentials{
+ // AccessKeyID: providerOptions.accessKey,
+ // SecretAccessKey: providerOptions.secretKey,
+ // SessionToken: providerOptions.sessionToken,
+ // }, nil
+ // },
+ // )
+ // }
+
+ client := bedrockruntime.NewFromConfig(
+ cfg,
+ func(o *bedrockruntime.Options) {
+ if providerOptions.httpClient != nil {
+ o.HTTPClient = providerOptions.httpClient
+ }
+ },
+ )
+
+ return &provider{
+ options: providerOptions,
+ client: client,
+ }, nil
+}
+
+func WithName(name string) Option {
+ return func(o *options) {
+ o.name = name
+ }
+}
+
+func WithHTTPClient(httpClient bedrockruntime.HTTPClient) Option {
+ return func(o *options) {
+ o.httpClient = httpClient
+ }
+}
+
+// func WithRegion(region string) Option {
+// return func(o *options) {
+// o.region = region
+// }
+// }
+
+// func WithCredentials(accessKey, secretKey, sessionToken string) Option {
+// return func(o *options) {
+// o.accessKey = accessKey
+// o.secretKey = secretKey
+// o.sessionToken = sessionToken
+// }
+// }
+
+// func WithEndpoint(endpoint string) Option {
+// return func(o *options) {
+// o.endpoint = endpoint
+// }
+// }
+
+func (b *provider) Name() string {
+ return Name
+}
+
+func (b *provider) LanguageModel(modelID string) (ai.LanguageModel, error) {
+ return languageModel{
+ modelID: modelID,
+ provider: b.options.name,
+ client: b.client,
+ }, nil
+}
+
+func (b *provider) ParseOptions(data map[string]any) (ai.ProviderOptionsData, error) {
+ var options ProviderOptions
+ if err := ai.ParseOptions(data, &options); err != nil {
+ return nil, err
+ }
+ return &options, nil
+}
@@ -0,0 +1,54 @@
+package bedrock
+
+import (
+ "fmt"
+
+ "github.com/charmbracelet/fantasy/ai"
+)
+
+// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-text-completion.html
+// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
+
+type claudeInput struct {
+ AnthropicVersion string `json:"anthropic_version"`
+ MaxTokens *int64 `json:"max_tokens"`
+ Messages []claudeMessage `json:"messages"`
+}
+
+type claudeMessage struct {
+ Role string `json:"role"`
+ Content string `json:"content"`
+}
+
+func toClaudeInput(call ai.Call) (claudeInput, error) {
+ var err error
+ i := claudeInput{
+ MaxTokens: call.MaxOutputTokens,
+ }
+
+ i.Messages, err = toClaudePrompt(call.Prompt)
+ if err != nil {
+ return i, err
+ }
+ return i, nil
+}
+
+func toClaudePrompt(prompt ai.Prompt) (messages []claudeMessage, err error) {
+ for _, m := range prompt {
+ message := claudeMessage{
+ Role: string(m.Role),
+ }
+
+ for _, part := range m.Content {
+ switch content := part.(type) {
+ case ai.TextPart:
+ message.Content = content.Text
+ default:
+ return nil, fmt.Errorf("fantasy: ")
+ }
+ }
+
+ messages = append(messages, message)
+ }
+ return messages, err
+}
@@ -0,0 +1,85 @@
+package bedrock
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
+ "github.com/charmbracelet/fantasy/ai"
+)
+
+type languageModel struct {
+ provider string
+ modelID string
+ client *bedrockruntime.Client
+}
+
+func (b languageModel) Model() string {
+ return b.modelID
+}
+
+func (b languageModel) Provider() string {
+ return b.provider
+}
+
+func (b languageModel) Generate(ctx context.Context, call ai.Call) (*ai.Response, error) {
+ params, err := b.prepareParams(call)
+ if err != nil {
+ return nil, err
+ }
+
+ output, err := b.client.InvokeModel(ctx, params)
+ if err != nil {
+ return nil, err
+ }
+
+ panic(fmt.Sprintf("bedrock output: %+v", output))
+
+ // return &ai.Response{
+ // Content: content,
+ // // Usage: ai.Usage{
+ // // InputTokens: response.Usage.InputTokens,
+ // // OutputTokens: response.Usage.OutputTokens,
+ // // TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
+ // // CacheCreationTokens: response.Usage.CacheCreationInputTokens,
+ // // CacheReadTokens: response.Usage.CacheReadInputTokens,
+ // // },
+ // // FinishReason: mapFinishReason(string(response.StopReason)),
+ // ProviderMetadata: ai.ProviderMetadata{},
+ // // Warnings: warnings,
+ // }, nil
+}
+
+func (b languageModel) Stream(ctx context.Context, call ai.Call) (ai.StreamResponse, error) {
+ return nil, errors.New("bedrock provider not fully implemented")
+}
+
+func (b languageModel) prepareParams(call ai.Call) (*bedrockruntime.InvokeModelInput, error) {
+ input := bedrockruntime.InvokeModelInput{
+ // ModelId: ptr(fmt.Sprintf("us-east-1.%s", b.modelID)),
+ ModelId: ptr(b.modelID),
+ // ModelId: ptr("us-east-1.anthropic.claude-sonnet-4-5-v2:0"),
+ ContentType: ptr("application/json"),
+ // Body: body,
+ }
+
+ // call.Prompt
+
+ switch {
+ case containsAny(b.modelID, "anthropic", "claude", "sonnet"):
+ i, err := toClaudeInput(call)
+ if err != nil {
+ return nil, err
+ }
+ input.Body, err = json.Marshal(i)
+ if err != nil {
+ return nil, err
+ }
+ default:
+ return nil, fmt.Errorf("fantasy: bedrock provider does not support model: %s", b.modelID)
+ }
+
+ return &input, nil
+}
@@ -0,0 +1,18 @@
+package bedrock
+
+import (
+ "strings"
+)
+
+func ptr[T any](v T) *T {
+ return &v
+}
+
+func containsAny(str string, options ...string) bool {
+ for _, option := range options {
+ if strings.Contains(str, option) {
+ return true
+ }
+ }
+ return false
+}
@@ -0,0 +1,15 @@
+package bedrock
+
+import "github.com/charmbracelet/fantasy/ai"
+
+type ProviderOptions struct {
+ // Add Bedrock-specific options here
+}
+
+func (o *ProviderOptions) Options() {}
+
+func NewProviderOptions(opts *ProviderOptions) ai.ProviderOptions {
+ return ai.ProviderOptions{
+ Name: opts,
+ }
+}
@@ -5,6 +5,8 @@ go 1.24.5
require (
cloud.google.com/go/auth v0.9.3
github.com/anthropics/anthropic-sdk-go v1.10.0
+ github.com/aws/aws-sdk-go-v2/config v1.27.27
+ github.com/aws/aws-sdk-go-v2/service/bedrock v1.47.2
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5
github.com/charmbracelet/x/json v0.2.0
github.com/go-viper/mapstructure/v2 v2.4.0
@@ -24,6 +26,20 @@ require (
cloud.google.com/go/compute/metadata v0.5.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.39.2 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.41.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
+ github.com/aws/smithy-go v1.23.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
@@ -16,6 +16,38 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkY
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I=
+github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
+github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
+github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
+github.com/aws/aws-sdk-go-v2/service/bedrock v1.47.2 h1:5Bq66lHNfiPHM9WBwzQfhqqctRTxXF3+Un1bm9ZyThE=
+github.com/aws/aws-sdk-go-v2/service/bedrock v1.47.2/go.mod h1:3sUHFSHdoib4v7JdqEGgxD2sIdTDikr4IpjBOgUAa0g=
+github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.41.0 h1:xdYdX+JpIFByMG8JQe9iWM9CqepyjhenukxTVQnuCbM=
+github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.41.0/go.mod h1:c1Ik+59wgLIJFhsSY8cAnw6QooiogpTZKP0rtkVcpCQ=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
+github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
+github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251010172108-7b952cdeeb9d h1:qP7F7r7aVY7AReYHHgkQ79weuUEZK7zXtDtSEydYb0w=
github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251010172108-7b952cdeeb9d/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
@@ -0,0 +1,47 @@
+package providertests
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/charmbracelet/fantasy/ai"
+ "github.com/charmbracelet/fantasy/bedrock"
+ "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
+)
+
+// const defaultBaseURL = "https://fantasy-playground-resource.services.ai.azure.com/"
+
+func TestBedrockCommon(t *testing.T) {
+ testCommon(t, []builderPair{
+ {"bedrock-anthropic-claude-v2", builderBedrockClaudeV2(t), nil},
+ })
+}
+
+// func TestBedrockThinking(t *testing.T) {
+// opts := ai.ProviderOptions{
+// bedrock.Name: &bedrock.ProviderOptions{
+// ReasoningEffort: openai.ReasoningEffortOption(openai.ReasoningEffortLow),
+// },
+// }
+// testThinking(t, []builderPair{
+// {"bedrock-anthropic-claude-v2", builderBedrockClaudeV2(t), opts},
+// }, testBedrockThinking)
+// }
+
+// func testBedrockThinking(t *testing.T, result *ai.AgentResult) {
+// require.Greater(t, result.Response.Usage.ReasoningTokens, int64(0), "expected reasoning tokens, got none")
+// }
+
+func builderBedrockClaudeV2(t *testing.T) func(r *recorder.Recorder) (ai.LanguageModel, error) {
+ return func(r *recorder.Recorder) (ai.LanguageModel, error) {
+ provider, err := bedrock.New(
+ t.Context(),
+ bedrock.WithHTTPClient(&http.Client{Transport: r}),
+ )
+ if err != nil {
+ return nil, err
+ }
+ // return provider.LanguageModel("anthropic.claude-sonnet-4-5-20250929-v1:0")
+ return provider.LanguageModel("us.anthropic.claude-3-sonnet-20240229-v1:0")
+ }
+}