Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0caebf5
feat(coderd): resolve bedrock inference profiles when a provider is w…
evgeniy-scherbina Sep 8, 2026
7d51f6d
refactor(aibridge/provider): resolve profiles from an explicit bedroc…
evgeniy-scherbina Sep 8, 2026
2fde4d3
Revert "refactor(aibridge/provider): resolve profiles from an explici…
evgeniy-scherbina Sep 8, 2026
493b668
refactor(coderd): share the bedrock settings converter with the write…
evgeniy-scherbina Sep 8, 2026
0774d48
refactor(coderd): resolve Bedrock profiles directly in provider writes
evgeniy-scherbina Sep 8, 2026
96adb4e
refactor(coderd): store bedrock profile resolution after the provider…
evgeniy-scherbina Sep 8, 2026
0ba3ad9
refactor(coderd): store bedrock profile resolution in its own table
evgeniy-scherbina Sep 9, 2026
5f4d2a3
refactor(aibridge): derive bedrock model identity from the provider c…
evgeniy-scherbina Sep 9, 2026
5a04d32
docs: minor changes
evgeniy-scherbina Sep 9, 2026
42f9cf3
refactor(aibridge/config): add resolved model fallback accessors
evgeniy-scherbina Sep 9, 2026
051d2b5
refactor(aibridge/config): simplify resolved model fallback accessors
evgeniy-scherbina Sep 9, 2026
a56f048
docs(aibridge/config): drop redundant accessor comment
evgeniy-scherbina Sep 9, 2026
4e555de
refactor(codersdk): carry bedrock model resolution on the settings type
evgeniy-scherbina Sep 9, 2026
b6c5568
refactor(coderd): key bedrock model resolution by inference profile arn
evgeniy-scherbina Sep 9, 2026
03bfa2c
refactor(coderd): report any bedrock resolution failure as a bad request
evgeniy-scherbina Sep 9, 2026
b1fdf6c
docs(coderd): trim bedrock resolution comments
evgeniy-scherbina Sep 9, 2026
886dd2e
revert(docs): restore the bedrock inference profile section
evgeniy-scherbina Sep 10, 2026
e68042a
refactor(coderd): store bedrock model resolution in provider settings…
evgeniy-scherbina Sep 10, 2026
7a5d01b
chore(coderd/database): drop generated remnants of the resolved model…
evgeniy-scherbina Sep 10, 2026
11ba473
refactor(coderd): resolve bedrock profiles before the provider write …
evgeniy-scherbina Sep 10, 2026
1e1a54d
docs(coderd): drop stale comment in the provider update handler
evgeniy-scherbina Sep 10, 2026
a417d6a
fix(coderd): authorize AI provider writes before resolving bedrock pr…
evgeniy-scherbina Sep 10, 2026
b5d4195
docs(coderd): clarify bedrock resolution endpoint
evgeniy-scherbina Sep 10, 2026
a291c80
refactor(cli): drop the bedrock config wrapper
evgeniy-scherbina Sep 11, 2026
9d5979b
refactor(coderd): rename the merged settings variable in provider upd…
evgeniy-scherbina Sep 11, 2026
7662f0f
Merge remote-tracking branch 'origin/main' into yevhenii/aigov-488-re…
evgeniy-scherbina Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions aibridge/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ type AWSBedrock struct {
// Protocol selects the Bedrock wire protocol. The zero value behaves as
// BedrockProtocolInvokeModel.
Protocol BedrockProtocol
// ResolvedModel is the model ID behind Model, which differs from it only
// when Model is an application inference profile ARN. coderd resolves it
// when the provider is written, so the gateway never calls AWS for it.
ResolvedModel string
// ResolvedSmallFastModel is ResolvedModel for SmallFastModel.
ResolvedSmallFastModel string
}

// ResolvedProtocol returns the configured protocol, mapping the empty value to
Expand All @@ -81,6 +87,20 @@ func (c AWSBedrock) ResolvedProtocol() BedrockProtocol {
return c.Protocol
}

func (c AWSBedrock) ResolvedModelWithFallback() string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-7] Removing gateway-side resolution while leaving every stored row's ResolvedModel empty means existing AIP providers silently degrade on upgrade, with no backfill and no operator signal. (Hisoka P3, Mafuuu P3, Chopper P3, Meruem P3, Pariston Note)

ResolvedModelWithFallback returns Model when ResolvedModel == "", and every row that exists before this PR has ResolvedModel == "". ... capability detection, pricing, usage, and metrics all key off the ARN string instead of the model ID. The PR's own stated failure mode returns: a Bedrock 400 on adaptive-only models.

Strong convergence from five reviewers. The fallback collapses two distinct states — "plain model ID, no resolution needed" and "AIP ARN never resolved" — into "serve the configured string." For the second, capability/pricing/usage/metrics key off the raw ARN and adaptive-only models revert to Bedrock 400s indefinitely until an operator re-saves each provider. Severity held at P3 (not higher) because #28877 is in no release tag (git tag --contains empty), so the exposed population is main/dogfood rows created between the two merges, and the state self-heals on any re-save. Blast radius narrow, teeth real. This needs a human decision, not a silent default: a first-write backfill, a release note telling operators to re-save AIP providers, or at minimum a warning log at gateway construction when a served Bedrock provider's identifier is an AIP ARN with empty resolution. ResolvedModelWithFallback already has the type in hand to tell the two cases apart via isApplicationInferenceProfileARN(c.Model).

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous functionality hadn't been released yet, so it should be okay.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-11] ResolvedModelWithFallback and ResolvedSmallFastModelWithFallback are exported with no doc comment, and hand-roll a fallback that cmp.Or covers. (Gon, Leorio, Ging-Go)

Both are exported methods on AWSBedrock and their whole reason to exist is non-obvious: they return the raw configured Model/SmallFastModel when the resolved value is empty (plain model IDs and pre-#28877 providers). (Gon)

AGENTS.md requires doc comments on exported symbols, and the sibling methods on this type (ResolvedProtocol, Validate) carry them. Ging-Go adds that return cmp.Or(c.ResolvedModel, c.Model) replaces the if/return (Go 1.22+, go.mod declares 1.26; the codebase already uses cmp.Or this way). Suggested:

// ResolvedModelWithFallback returns the resolved model ID, or the configured
// Model when no resolution is stored. Model is already a model identity for
// plain model IDs and for providers saved before resolution existed.
func (c AWSBedrock) ResolvedModelWithFallback() string {
	return cmp.Or(c.ResolvedModel, c.Model)
}

🤖

if c.ResolvedModel != "" {
return c.ResolvedModel
}
return c.Model
}

func (c AWSBedrock) ResolvedSmallFastModelWithFallback() string {
if c.ResolvedSmallFastModel != "" {
return c.ResolvedSmallFastModel
}
return c.SmallFastModel
}

// Validate verifies protocol-specific Bedrock configuration.
func (c AWSBedrock) Validate() error {
switch c.ResolvedProtocol() {
Expand Down
21 changes: 6 additions & 15 deletions aibridge/intercept/messages/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,22 +72,13 @@ var bedrockSupportedBetaFlags = map[string]bool{
type BedrockRuntime struct {
Cfg aibconfig.AWSBedrock
Creds aws.CredentialsProvider

resolvedModel string
resolvedSmallFastModel string
}

// NewBedrockRuntime bundles the Bedrock config and credentials with the model
// IDs behind the configured identifiers. The resolved IDs differ from the
// configured ones only when those are application inference profile ARNs, which
// are opaque and must be resolved through AWS; every other identifier resolves
// to itself.
func NewBedrockRuntime(cfg aibconfig.AWSBedrock, creds aws.CredentialsProvider, resolvedModel, resolvedSmallFastModel string) *BedrockRuntime {
// NewBedrockRuntime bundles the Bedrock config and credentials.
func NewBedrockRuntime(cfg aibconfig.AWSBedrock, creds aws.CredentialsProvider) *BedrockRuntime {
return &BedrockRuntime{
Cfg: cfg,
Creds: creds,
resolvedModel: resolvedModel,
resolvedSmallFastModel: resolvedSmallFastModel,
Cfg: cfg,
Creds: creds,
}
}

Expand All @@ -108,13 +99,13 @@ func (b *BedrockRuntime) ConfiguredSmallFastModel() string {
// Model capabilities, usage records, pricing, and metrics all key off this
// rather than the configured identifier.
func (b *BedrockRuntime) ResolvedModel() string {
return b.resolvedModel
return b.Cfg.ResolvedModelWithFallback()
}

// ResolvedSmallFastModel is [BedrockRuntime.ResolvedModel] for the small/fast
// model.
func (b *BedrockRuntime) ResolvedSmallFastModel() string {
return b.resolvedSmallFastModel
return b.Cfg.ResolvedSmallFastModelWithFallback()
}

type interceptionBase struct {
Expand Down
38 changes: 19 additions & 19 deletions aibridge/intercept/messages/base_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func TestAWSBedrockValidation(t *testing.T) {
t.Parallel()

base := &interceptionBase{
bedrock: NewBedrockRuntime(tt.cfg, credentials.NewStaticCredentialsProvider("test-key", "test-secret", ""), "", ""),
bedrock: NewBedrockRuntime(tt.cfg, credentials.NewStaticCredentialsProvider("test-key", "test-secret", "")),
}
opts, err := base.withBedrockInvokeModelOptions(context.Background())

Expand Down Expand Up @@ -217,9 +217,11 @@ func TestModelForBedrockInvokeModel(t *testing.T) {
)

runtime := NewBedrockRuntime(config.AWSBedrock{
Model: profileARN,
SmallFastModel: smallFastProfileARN,
}, nil, "anthropic.claude-opus-4-8", "anthropic.claude-haiku-4-5")
Model: profileARN,
SmallFastModel: smallFastProfileARN,
ResolvedModel: "anthropic.claude-opus-4-8",
ResolvedSmallFastModel: "anthropic.claude-haiku-4-5",
}, nil)

tests := []struct {
name string
Expand Down Expand Up @@ -274,9 +276,11 @@ func TestSmallFastModelCapturedAtConstruction(t *testing.T) {
)

runtime := NewBedrockRuntime(config.AWSBedrock{
Model: profileARN,
SmallFastModel: smallFastProfileARN,
}, nil, "anthropic.claude-opus-4-8", "anthropic.claude-haiku-4-5")
Model: profileARN,
SmallFastModel: smallFastProfileARN,
ResolvedModel: "anthropic.claude-opus-4-8",
ResolvedSmallFastModel: "anthropic.claude-haiku-4-5",
}, nil)

const haikuPayload = `{"model":"claude-haiku-4-5","max_tokens":10000}`
const opusPayload = `{"model":"claude-opus-4-8","max_tokens":10000}`
Expand Down Expand Up @@ -336,7 +340,7 @@ func TestModelForPlainBedrockModelID(t *testing.T) {
bedrock: NewBedrockRuntime(config.AWSBedrock{
Model: "eu.anthropic.claude-opus-4-8",
SmallFastModel: "anthropic.claude-haiku-4-5",
}, nil, "eu.anthropic.claude-opus-4-8", "anthropic.claude-haiku-4-5"),
}, nil),
logger: slog.Make(),
}

Expand Down Expand Up @@ -958,18 +962,14 @@ func TestAugmentRequestForBedrock_AdaptiveThinking(t *testing.T) {
}

// Plain model IDs resolve to themselves; an application inference
// profile ARN resolves to the model behind it.
resolvedModel := tc.resolvedModel
if resolvedModel == "" {
resolvedModel = tc.bedrockModel
}

i := &interceptionBase{
reqPayload: mustMessagesPayload(t, tc.requestBody),
bedrock: NewBedrockRuntime(config.AWSBedrock{
Model: tc.bedrockModel,
SmallFastModel: "anthropic.claude-haiku-3-5",
}, nil, resolvedModel, "anthropic.claude-haiku-3-5"),
Model: tc.bedrockModel,
SmallFastModel: "anthropic.claude-haiku-3-5",
ResolvedModel: tc.resolvedModel,
ResolvedSmallFastModel: "anthropic.claude-haiku-3-5",
}, nil),
clientHeaders: clientHeaders,
logger: slog.Make(),
}
Expand Down Expand Up @@ -1322,7 +1322,7 @@ func TestBedrockMantleIsPassthrough(t *testing.T) {
Region: "us-east-1",
BaseURL: "https://bedrock-mantle.us-east-1.api.aws/anthropic",
Protocol: config.BedrockProtocolMantle,
}, credentials.NewStaticCredentialsProvider("test-key", "test-secret", ""), "", ""),
}, credentials.NewStaticCredentialsProvider("test-key", "test-secret", "")),
logger: slog.Make(),
}

Expand Down Expand Up @@ -1373,7 +1373,7 @@ func TestAWSMantleOptionsValidation(t *testing.T) {
t.Parallel()

base := &interceptionBase{
bedrock: NewBedrockRuntime(tt.cfg, credentials.NewStaticCredentialsProvider("test-key", "test-secret", ""), "", ""),
bedrock: NewBedrockRuntime(tt.cfg, credentials.NewStaticCredentialsProvider("test-key", "test-secret", "")),
}
opts, err := base.withBedrockMantleOptions(t.Context())
if tt.errorMsg != "" {
Expand Down
11 changes: 1 addition & 10 deletions aibridge/provider/anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,7 @@ func NewAnthropic(ctx context.Context, cfg config.Anthropic, bedrockCfg *config.
return nil, xerrors.Errorf("bedrock config: %w", err)
}

// Resolution only calls AWS for application inference profile ARNs, so
// deployments configured with plain model IDs need no extra permission.
resolveCtx, cancel := context.WithTimeout(ctx, inferenceProfileResolutionTimeout)
defer cancel()
model, smallFastModel, err := resolveBedrockModels(resolveCtx, runtimeCfg, awsCfg)
if err != nil {
return nil, xerrors.Errorf("resolve bedrock models: %w", err)
}

bedrock = messages.NewBedrockRuntime(runtimeCfg, awsCfg.Credentials, model, smallFastModel)
bedrock = messages.NewBedrockRuntime(runtimeCfg, awsCfg.Credentials)
}

return &Anthropic{
Expand Down
47 changes: 32 additions & 15 deletions aibridge/provider/bedrock_inference_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,25 +88,42 @@ func modelIDFromARN(modelARN string) (string, error) {
return model, nil
}

// resolveBedrockModels resolves the configured model identifiers to the model
// IDs used for capability detection, usage recording, and pricing. Identifiers
// that are not application inference profile ARNs are returned unchanged and
// cost no AWS call.
func resolveBedrockModels(ctx context.Context, cfg config.AWSBedrock, awsCfg aws.Config) (model, smallFastModel string, err error) {
resolveOne := func(configured string) (string, error) {
if !isApplicationInferenceProfileARN(configured) {
return configured, nil
// ResolveBedrockModels resolves the application inference profile ARNs among
// the configured model identifiers, returning what each ARN refers to. The
// result is empty when neither identifier is an ARN, which costs no AWS call.
//
// The identity comes from cfg, including any role assumed via config.AWSBedrock.RoleARN,
// so the required bedrock:GetInferenceProfile permission belongs to that identity.
func ResolveBedrockModels(ctx context.Context, cfg config.AWSBedrock) (map[string]string, error) {
resolved := make(map[string]string, 2)

var profiles []string
for _, configured := range []string{cfg.Model, cfg.SmallFastModel} {
if isApplicationInferenceProfileARN(configured) {
profiles = append(profiles, configured)
}
return resolveInferenceProfile(ctx, awsCfg, configured)
}
if len(profiles) == 0 {
return resolved, nil
}

model, err = resolveOne(cfg.Model)
awsCfg, err := buildBedrockCredentials(ctx, cfg)
if err != nil {
return "", "", xerrors.Errorf("resolve model: %w", err)
return nil, xerrors.Errorf("build bedrock credentials: %w", err)
}
smallFastModel, err = resolveOne(cfg.SmallFastModel)
if err != nil {
return "", "", xerrors.Errorf("resolve small fast model: %w", err)

resolveCtx, cancel := context.WithTimeout(ctx, inferenceProfileResolutionTimeout)
defer cancel()

for _, profileARN := range profiles {
if _, ok := resolved[profileARN]; ok {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note [CRF-2] The dedup branch (if _, ok := resolved[profileARN]; ok { continue }) is untested. (Netero, Bisky)

ResolveBedrockModels reads 90.0% covered; no test configures the same profile ARN for both Model and SmallFastModel, so the branch that avoids a second identical lookup never executes under test.

Low value: a one-line guard with obvious behavior. Noted for completeness.

🤖

continue
}
model, err := resolveInferenceProfile(resolveCtx, awsCfg, profileARN)
if err != nil {
return nil, err
}
resolved[profileARN] = model
}
return model, smallFastModel, nil
return resolved, nil
}
Loading
Loading