mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user