-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat: support cross-account Bedrock AssumeRole in AI Bridge #26527
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
Changes from all commits
dbf927d
dfd5c9f
65cb77c
511e209
3e671c5
5aece13
55ee6cc
0d6cb16
7dcf5db
eec5a92
a4277e5
8769c21
8597d74
167ad2d
9bc4146
37529bd
2fd0b14
5ef859f
b07ef44
9b436d6
4ea714e
ed11c8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package aibridgetest | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/coder/coder/v2/aibridge" | ||
| ) | ||
|
|
||
| // NewAnthropicProvider builds an Anthropic provider for tests, failing the test | ||
| // if credential resolution fails. | ||
| func NewAnthropicProvider(t testing.TB, cfg aibridge.AnthropicConfig, bedrockCfg *aibridge.AWSBedrockConfig) aibridge.Provider { | ||
| t.Helper() | ||
| p, err := aibridge.NewAnthropicProvider(context.Background(), cfg, bedrockCfg) | ||
| require.NoError(t, err) | ||
| return p | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,8 +16,7 @@ import ( | |
| "github.com/anthropics/anthropic-sdk-go/option" | ||
| "github.com/anthropics/anthropic-sdk-go/shared" | ||
| "github.com/anthropics/anthropic-sdk-go/shared/constant" | ||
| "github.com/aws/aws-sdk-go-v2/config" | ||
| "github.com/aws/aws-sdk-go-v2/credentials" | ||
| "github.com/aws/aws-sdk-go-v2/aws" | ||
| "github.com/google/uuid" | ||
| "go.opentelemetry.io/otel/attribute" | ||
| "go.opentelemetry.io/otel/trace" | ||
|
|
@@ -65,13 +64,26 @@ var bedrockSupportedBetaFlags = map[string]bool{ | |
| "tool-examples-2025-10-29": true, | ||
| } | ||
|
|
||
| // BedrockRuntime carries everything a Bedrock-backed interception needs: the | ||
| // static Bedrock config plus the AWS credentials provider. | ||
| type BedrockRuntime struct { | ||
| Cfg aibconfig.AWSBedrock | ||
| Creds aws.CredentialsProvider | ||
| // ResolvedRegion is the region the AWS SDK resolved at construction (from | ||
| // the environment, shared config, or IMDS). It is used for request signing | ||
| // when Cfg.Region is empty, e.g. a custom base URL with the region supplied | ||
| // via AWS_REGION. | ||
| ResolvedRegion string | ||
| } | ||
|
|
||
| type interceptionBase struct { | ||
| id uuid.UUID | ||
| reqPayload RequestPayload | ||
|
|
||
| cfg intercept.Config | ||
| cred intercept.Credential | ||
| bedrockCfg *aibconfig.AWSBedrock | ||
| cfg intercept.Config | ||
| cred intercept.Credential | ||
| // bedrock is nil for non-Bedrock providers. | ||
| bedrock *BedrockRuntime | ||
|
dannykopping marked this conversation as resolved.
|
||
|
|
||
| // clientHeaders are the original HTTP headers from the client request. | ||
| clientHeaders http.Header | ||
|
|
@@ -107,10 +119,10 @@ func (i *interceptionBase) Model() string { | |
| return "coder-aibridge-unknown" | ||
| } | ||
|
|
||
| if i.bedrockCfg != nil { | ||
| model := i.bedrockCfg.Model | ||
| if i.bedrock != nil { | ||
| model := i.bedrock.Cfg.Model | ||
| if i.isSmallFastModel() { | ||
| model = i.bedrockCfg.SmallFastModel | ||
| model = i.bedrock.Cfg.SmallFastModel | ||
| } | ||
| return model | ||
| } | ||
|
|
@@ -126,7 +138,7 @@ func (i *interceptionBase) baseTraceAttributes(r *http.Request, streaming bool) | |
| attribute.String(tracing.Provider, i.cfg.ProviderName), | ||
| attribute.String(tracing.Model, i.Model()), | ||
| attribute.Bool(tracing.Streaming, streaming), | ||
| attribute.Bool(tracing.IsBedrock, i.bedrockCfg != nil), | ||
| attribute.Bool(tracing.IsBedrock, i.bedrock != nil), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -238,10 +250,10 @@ func (i *interceptionBase) newMessagesService(ctx context.Context, opts ...optio | |
| opts = append(opts, option.WithMiddleware(mw)) | ||
| } | ||
|
|
||
| if i.bedrockCfg != nil { | ||
| if i.bedrock != nil { | ||
| ctx, cancel := context.WithTimeout(ctx, time.Second*30) | ||
| defer cancel() | ||
| bedrockOpts, err := i.withAWSBedrockOptions(ctx, i.bedrockCfg) | ||
| bedrockOpts, err := i.withAWSBedrockOptions(ctx) | ||
| if err != nil { | ||
| return anthropic.MessageService{}, err | ||
| } | ||
|
|
@@ -262,14 +274,13 @@ func (i *interceptionBase) withBody() option.RequestOption { | |
|
|
||
| // withAWSBedrockOptions returns request options for authenticating with AWS Bedrock. | ||
| // | ||
| // When both AccessKey and AccessKeySecret are set in the aibridge config, they are | ||
| // used directly as static credentials. Otherwise, the AWS SDK default credential chain | ||
| // resolves credentials (environment variables, shared config/credentials files, IAM | ||
| // roles, IRSA, SSO, IMDS, etc.). | ||
| func (*interceptionBase) withAWSBedrockOptions(ctx context.Context, cfg *aibconfig.AWSBedrock) ([]option.RequestOption, error) { | ||
| if cfg == nil { | ||
| return nil, xerrors.New("nil config given") | ||
| // Credentials come from i.bedrock.Creds. It is a shared credentials cache, so the per-request Retrieve() | ||
| // below is served from that cache and does not re-resolve or re-assume on every request. | ||
| func (i *interceptionBase) withAWSBedrockOptions(ctx context.Context) ([]option.RequestOption, error) { | ||
| if i.bedrock == nil { | ||
| return nil, xerrors.New("nil bedrock runtime") | ||
| } | ||
| cfg := i.bedrock.Cfg | ||
| if cfg.Region == "" && cfg.BaseURL == "" { | ||
| return nil, xerrors.New("region or base url required") | ||
| } | ||
|
|
@@ -280,38 +291,22 @@ func (*interceptionBase) withAWSBedrockOptions(ctx context.Context, cfg *aibconf | |
| return nil, xerrors.New("small fast model required") | ||
| } | ||
|
|
||
| loadOpts := []func(*config.LoadOptions) error{ | ||
| config.WithRegion(cfg.Region), | ||
| } | ||
|
|
||
| // Use static credentials when explicitly provided, otherwise fall back to the SDK default credential chain. | ||
| switch { | ||
| // Both set: use static credentials directly. | ||
| case cfg.AccessKey != "" && cfg.AccessKeySecret != "": | ||
| loadOpts = append(loadOpts, config.WithCredentialsProvider( | ||
| credentials.NewStaticCredentialsProvider( | ||
| cfg.AccessKey, | ||
| cfg.AccessKeySecret, | ||
| "", | ||
| ), | ||
| )) | ||
| // Only one set: misconfiguration. | ||
| case cfg.AccessKey != "" || cfg.AccessKeySecret != "": | ||
| return nil, xerrors.New("both access key and access key secret must be provided together") | ||
| // Neither set: SDK default credential chain resolves credentials. | ||
| default: | ||
| // Fail fast: ensure credentials can be resolved before signing. Served from | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3 [CRF-18] "Served from the shared cache on most requests (no network); on the cold or refresh path this performs the actual STS/IMDS call." This restates the caching behavior already documented on Suggested: (Gon)
|
||
| // the shared cache on most requests (no network); on the cold or refresh | ||
| // path this performs the actual STS/IMDS call. | ||
| if _, err := i.bedrock.Creds.Retrieve(ctx); err != nil { | ||
| return nil, xerrors.Errorf("resolve AWS credentials: %w", err) | ||
| } | ||
|
|
||
| awsCfg, err := config.LoadDefaultConfig(ctx, loadOpts...) | ||
| if err != nil { | ||
| return nil, xerrors.Errorf("failed to load AWS Bedrock config: %w", err) | ||
| // Fall back to the SDK-resolved region (e.g. from AWS_REGION) when no | ||
| // explicit region is configured. | ||
| region := cfg.Region | ||
| if region == "" { | ||
| region = i.bedrock.ResolvedRegion | ||
| } | ||
|
|
||
| // Fail fast: ensure credentials can be resolved before making any requests. | ||
| // awsCfg already carries the credentials provider, and the Bedrock middleware | ||
| // will call Retrieve on it when signing each request. | ||
| if _, err := awsCfg.Credentials.Retrieve(ctx); err != nil { | ||
| return nil, xerrors.Errorf("no AWS credentials found: %w", err) | ||
| awsCfg := aws.Config{ | ||
|
dannykopping marked this conversation as resolved.
|
||
| Region: region, | ||
| Credentials: i.bedrock.Creds, | ||
| } | ||
|
|
||
| var out []option.RequestOption | ||
|
|
@@ -336,7 +331,7 @@ func (*interceptionBase) withAWSBedrockOptions(ctx context.Context, cfg *aibconf | |
| // don't support adaptive thinking natively, or enabled thinking to adaptive for models that only support | ||
| // adaptive (Opus 4.7+). | ||
| func (i *interceptionBase) augmentRequestForBedrock() { | ||
| if i.bedrockCfg == nil { | ||
| if i.bedrock == nil { | ||
| return | ||
| } | ||
|
|
||
|
|
||
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.
Why this change? It was more clear before.
Uh oh!
There was an error while loading. Please reload this page.
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.
Hint should be under 15 chars, according to DB constraint to prevent accidentally storing real credentials.
So this bug existed before.
But I think this bug wasn't released yet, probably I need to add test for it as well.
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.
Updated test here: 5ef859f to prevent it from happening in the future.