fix: require bedrock model fields for the invoke-model protocol (#27846)

Implements:
https://linear.app/codercom/issue/AIGOV-564/aibridge-bedrock-provider-skipped-404-on-all-routes-when-settings-omit

Improves validation when creating and updating AI providers: a Bedrock
provider using the `invoke-model` protocol now requires `model` and
`small_fast_model`.

This brings API validation in sync with the UI, which already required
both fields.
This commit is contained in:
Yevhenii Shcherbina
2026-08-04 15:21:23 -04:00
committed by GitHub
parent 10b366cb7c
commit 11427066a1
4 changed files with 240 additions and 21 deletions
+34
View File
@@ -285,6 +285,7 @@ func (req CreateAIProviderRequest) Validate() []ValidationError {
})
}
validations = append(validations, validateAIProviderBedrockMantleRegion(*req.Settings.Bedrock)...)
validations = append(validations, validateAIProviderBedrockModels(*req.Settings.Bedrock)...)
}
if req.Type == AIProviderTypeCopilot && len(req.APIKeys) > 0 {
validations = append(validations, ValidationError{
@@ -335,10 +336,17 @@ func (req UpdateAIProviderRequest) Validate() []ValidationError {
if req.APIKeys != nil {
validations = append(validations, validateAIProviderKeyMutations(*req.APIKeys)...)
}
// Despite arriving on a PATCH, a bedrock settings blob is a full
// replacement rather than a per-field patch: the caller must set every
// field, except AccessKey, AccessKeySecret, and ExternalID, which
// mergeAIProviderSettings carries forward from the stored row when
// omitted. Omitting any other field clears it, so the checks below apply
// to the patch exactly as they would to what gets stored.
if req.Settings != nil && req.Settings.Bedrock != nil {
validations = append(validations, validateAIProviderRoleARN(req.Settings.Bedrock.RoleARN)...)
validations = append(validations, validateAIProviderBedrockProtocol(req.Settings.Bedrock.Protocol)...)
validations = append(validations, validateAIProviderBedrockMantleRegion(*req.Settings.Bedrock)...)
validations = append(validations, validateAIProviderBedrockModels(*req.Settings.Bedrock)...)
}
return validations
}
@@ -385,6 +393,32 @@ func validateAIProviderBedrockMantleRegion(b AIProviderBedrockSettings) []Valida
return nil
}
// validateAIProviderBedrockModels requires the model identifiers that the
// invoke-model protocol substitutes into every upstream request. Without them
// the provider cannot be constructed at runtime (see
// config.AWSBedrock.Validate), so it would be skipped at gateway startup and
// every request to it would 404. The mantle protocol forwards the client's
// model unchanged and needs neither field.
func validateAIProviderBedrockModels(b AIProviderBedrockSettings) []ValidationError {
if b.ResolvedProtocol() != AIProviderBedrockProtocolInvokeModel {
return nil
}
var validations []ValidationError
if b.Model == "" {
validations = append(validations, ValidationError{
Field: "settings.model",
Detail: "model is required for the invoke-model protocol",
})
}
if b.SmallFastModel == "" {
validations = append(validations, ValidationError{
Field: "settings.small_fast_model",
Detail: "small_fast_model is required for the invoke-model protocol",
})
}
return validations
}
func validateAIProviderRoleARN(roleARN string) []ValidationError {
if roleARN == "" {
return nil
+3 -7
View File
@@ -76,13 +76,9 @@ func (b AIProviderBedrockSettings) ResolvedProtocol() AIProviderBedrockProtocol
// indicating that the operator wants the provider to authenticate via
// AWS Bedrock rather than as a bearer-token Anthropic provider.
//
// Model and SmallFastModel are intentionally excluded: they have
// deployment-level defaults declared in codersdk/deployment.go, so
// they're always non-empty in a real deployment and cannot serve as
// a detection signal. Region and credentials have no defaults and
// therefore reliably indicate operator intent. Credentials alone are
// not required because Bedrock can also authenticate via the AWS
// environment (instance profile, AWS_PROFILE, IRSA, etc.).
// Region and credentials have no defaults and therefore reliably indicate
// operator intent. Credentials alone are not required because Bedrock can
// also authenticate via the AWS environment (instance profile, AWS_PROFILE, IRSA, etc.).
func (b AIProviderBedrockSettings) IsConfigured() bool {
if b.Region != "" {
return true
+83
View File
@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/aibridge/config"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
)
@@ -323,3 +324,85 @@ func TestAIProviderRequest_ValidateBedrockMantle(t *testing.T) {
}
})
}
// TestAIProviderRequest_ValidationInSync keeps API-level validation
// (CreateAIProviderRequest.Validate) and runtime-level validation
// (config.AWSBedrock.Validate) in sync.
func TestAIProviderRequest_ValidationInSync(t *testing.T) {
t.Parallel()
const (
model = "anthropic.claude-sonnet-4-5"
smallFastModel = "anthropic.claude-haiku-4-5"
)
cases := []struct {
name string
baseURL string
bedrock codersdk.AIProviderBedrockSettings
isValid bool
}{
{
name: "InvokeModelRegionOnly",
baseURL: "https://bedrock.us-east-2.amazonaws.com",
bedrock: codersdk.AIProviderBedrockSettings{Region: "us-east-2"},
isValid: false,
},
{
name: "InvokeModelMissingSmallFastModel",
baseURL: "https://bedrock.us-east-2.amazonaws.com",
bedrock: codersdk.AIProviderBedrockSettings{
Region: "us-east-2",
Model: model,
},
isValid: false,
},
{
name: "InvokeModelComplete",
baseURL: "https://bedrock.us-east-2.amazonaws.com",
bedrock: codersdk.AIProviderBedrockSettings{
Region: "us-east-2",
Model: model,
SmallFastModel: smallFastModel,
},
isValid: true,
},
{
// Mantle forwards the client's model unchanged, so it needs neither.
name: "MantleWithoutModels",
baseURL: "https://bedrock-mantle.us-east-2.api.aws/anthropic",
bedrock: codersdk.AIProviderBedrockSettings{
Region: "us-east-2",
Protocol: codersdk.AIProviderBedrockProtocolMantle,
},
isValid: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Mirror the settings-to-runtime conversion that cli/aibridged.go
// performs when it builds providers from the database.
runtimeCfg := config.AWSBedrock{
BaseURL: tc.baseURL,
Region: tc.bedrock.Region,
Model: tc.bedrock.Model,
SmallFastModel: tc.bedrock.SmallFastModel,
Protocol: config.BedrockProtocol(tc.bedrock.ResolvedProtocol()),
}
require.Equal(t, tc.isValid, runtimeCfg.Validate() == nil,
"config.AWSBedrock.Validate disagrees with the expected verdict")
create := codersdk.CreateAIProviderRequest{
Type: codersdk.AIProviderTypeBedrock,
Name: "bedrock",
BaseURL: tc.baseURL,
Settings: codersdk.AIProviderSettings{Bedrock: &tc.bedrock},
}
require.Equal(t, tc.isValid, len(create.Validate()) == 0,
"the API disagrees with the expected verdict")
})
}
}