fix(bedrock): GetFoundationModel answers an unknown model with ValidationException (#1104)

This commit is contained in:
Julian Sommer
2026-09-24 16:22:33 +10:00
committed by GitHub
parent 372991c304
commit cb83f9b193
4 changed files with 32 additions and 34 deletions
+7 -13
View File
@@ -463,8 +463,7 @@ func LookupServingSpec(modelID string) (spec ServingSpec, found, selfHost bool)
// An unknown model and an ungranted one are deliberately distinguishable here
// (ResourceNotFoundException vs AccessDeniedException) because the caller has
// already been told the model exists by its own catalog listing; the describe
// path collapses both to ResourceNotFoundException instead, where that is not
// true.
// path collapses both to ValidationException instead, where that is not true.
func grantedCatalogEntry(ctx context.Context, accountID, modelID string, access AccessResolver) (catalogEntry, error) {
entry, ok := lookupCatalogEntry(modelID)
if !ok {
@@ -496,18 +495,13 @@ func ListFoundationModels(ctx context.Context, accountID string, resolver Creden
return &bedrock.ListFoundationModelsOutput{ModelSummaries: summaries}, nil
}
// GetFoundationModel looks up a single model by exact modelId, gated by the
// caller's grant. An ungranted model, and a self-host model with no resolvable
// weights snapshot, are both reported as ResourceNotFoundException rather than
// AccessDeniedException so describe agrees with list: a model the account
// cannot see does not exist as far as this API is concerned, and the error does
// not confirm the model's existence to an account probing for it. A weights
// resolve error is an internal fault, not a not-found verdict, and is surfaced
// (and logged) as such.
// GetFoundationModel describes one model. Unknown, ungranted and unservable
// models all fail with AWS's ValidationException so describe agrees with list
// and never confirms a hidden model exists; a weights resolve error is a fault.
func GetFoundationModel(ctx context.Context, accountID string, modelID string, access AccessResolver) (*bedrock.GetFoundationModelOutput, error) {
entry, ok := lookupCatalogEntry(modelID)
if !ok {
return nil, errors.New(awserrors.ErrorResourceNotFoundException)
return nil, awserrors.Errorf(awserrors.ErrorValidationException, "The provided model identifier is invalid.")
}
// Grant first: an ungranted account must not learn whether the model is
// servable, only that it is not there.
@@ -516,7 +510,7 @@ func GetFoundationModel(ctx context.Context, accountID string, modelID string, a
return nil, err
}
if !granted {
return nil, errors.New(awserrors.ErrorResourceNotFoundException)
return nil, awserrors.Errorf(awserrors.ErrorValidationException, "The provided model identifier is invalid.")
}
if entry.Provider == tierSelfHost {
_, resolvable, err := currentWeightsResolver().Resolve(ctx, entry.ModelID)
@@ -525,7 +519,7 @@ func GetFoundationModel(ctx context.Context, accountID string, modelID string, a
return nil, fmt.Errorf("resolve weights for %s: %w", entry.ModelID, err)
}
if !resolvable {
return nil, errors.New(awserrors.ErrorResourceNotFoundException)
return nil, awserrors.Errorf(awserrors.ErrorValidationException, "The provided model identifier is invalid.")
}
}
return &bedrock.GetFoundationModelOutput{ModelDetails: entry.toDetails()}, nil
+15 -11
View File
@@ -7,6 +7,7 @@ import (
"testing"
"github.com/aws/aws-sdk-go/service/bedrock"
"github.com/mulgadc/spinifex/spinifex/awserrors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -125,11 +126,11 @@ func TestGetFoundationModel_KnownModelWithResolvableWeights(t *testing.T) {
assert.Equal(t, selfHostTestModel, *out.ModelDetails.ModelId)
}
func TestGetFoundationModel_SelfHostWithUnresolvableWeightsReturnsNotFound(t *testing.T) {
func TestGetFoundationModel_SelfHostWithUnresolvableWeightsMatchesUnknown(t *testing.T) {
withWeightsResolver(t, stubWeightsResolver{ok: map[string]bool{}})
_, err := GetFoundationModel(context.Background(), "000000000001", selfHostTestModel, grantAll{})
require.Error(t, err)
assert.Equal(t, "ResourceNotFoundException", err.Error())
assert.True(t, awserrors.IsErrorCode(err, awserrors.ErrorValidationException))
}
// TestListFoundationModels_GrantDoesNotOverrideCredentialTier keeps the two
@@ -163,26 +164,29 @@ func TestGetFoundationModel_KnownGrantedModel(t *testing.T) {
assert.Equal(t, selfHostTestModel, *out.ModelDetails.ModelId)
}
func TestGetFoundationModel_UnknownModelReturnsNotFound(t *testing.T) {
func TestGetFoundationModel_UnknownModelReturnsValidation(t *testing.T) {
_, err := GetFoundationModel(context.Background(), "000000000001", "does-not-exist", grantAll{})
require.Error(t, err)
assert.Equal(t, "ResourceNotFoundException", err.Error())
code, message, ok := awserrors.ResolveErrorDetail(err)
require.True(t, ok)
assert.Equal(t, awserrors.ErrorValidationException, code)
assert.Equal(t, "The provided model identifier is invalid.", message)
}
// TestGetFoundationModel_UngrantedModelReturnsNotFound pins describe to the
// same answer as list: an ungranted model is reported as absent rather than
// forbidden, so the error cannot be used to confirm the model exists.
func TestGetFoundationModel_UngrantedModelReturnsNotFound(t *testing.T) {
// TestGetFoundationModel_UngrantedModelMatchesUnknown pins describe to the
// same answer as list: an ungranted model gets the unknown-model error rather
// than a denial, so the error cannot be used to confirm the model exists.
func TestGetFoundationModel_UngrantedModelMatchesUnknown(t *testing.T) {
_, err := GetFoundationModel(context.Background(), "000000000001", selfHostTestModel, grantSet{})
require.Error(t, err)
assert.Equal(t, "ResourceNotFoundException", err.Error())
assert.True(t, awserrors.IsErrorCode(err, awserrors.ErrorValidationException))
}
func TestGetFoundationModel_WeightsResolveErrorIsNotResourceNotFound(t *testing.T) {
func TestGetFoundationModel_WeightsResolveErrorIsNotValidation(t *testing.T) {
withWeightsResolver(t, errWeightsResolver{})
_, err := GetFoundationModel(context.Background(), "000000000001", selfHostTestModel, grantAll{})
require.Error(t, err)
assert.NotEqual(t, "ResourceNotFoundException", err.Error())
assert.False(t, awserrors.IsErrorCode(err, awserrors.ErrorValidationException))
}
func TestListFoundationModels_WeightsResolveErrorPropagates(t *testing.T) {
+5 -5
View File
@@ -126,12 +126,12 @@ func TestBedrockRequest_DenyByDefault(t *testing.T) {
assert.Empty(t, out.ModelSummaries)
})
t.Run("describe reports not found", func(t *testing.T) {
t.Run("describe reports it as unknown", func(t *testing.T) {
req := bedrockRequestWithAccount(http.MethodGet, "/foundation-models/"+bedrockTestLlamaModelID, "")
w := httptest.NewRecorder()
err := gw.Bedrock_Request(w, req)
require.Error(t, err)
assert.Equal(t, awserrors.ErrorResourceNotFoundException, err.Error())
assert.True(t, awserrors.IsErrorCode(err, awserrors.ErrorValidationException))
})
// Every runtime verb must refuse, not just the one that happens to be
@@ -187,7 +187,7 @@ func TestBedrockRequest_GetFoundationModel(t *testing.T) {
assert.Equal(t, bedrockTestLlamaModelID, *out.ModelDetails.ModelId)
}
func TestBedrockRequest_GetFoundationModel_NotFound(t *testing.T) {
func TestBedrockRequest_GetFoundationModel_UnknownModel(t *testing.T) {
ts := newVLLMStub(t)
gw := newBedrockRequestGateway(t, ts.URL)
@@ -195,7 +195,7 @@ func TestBedrockRequest_GetFoundationModel_NotFound(t *testing.T) {
w := httptest.NewRecorder()
err := gw.Bedrock_Request(w, req)
require.Error(t, err)
assert.Equal(t, awserrors.ErrorResourceNotFoundException, err.Error())
assert.True(t, awserrors.IsErrorCode(err, awserrors.ErrorValidationException))
}
func TestBedrockRequest_UnknownRouteReturnsInvalidAction(t *testing.T) {
@@ -249,7 +249,7 @@ func TestBedrockRuntimeRequest_MalformedBodyReturnsValidationException(t *testin
w := httptest.NewRecorder()
err := gw.BedrockRuntime_Request(w, req)
require.Error(t, err)
assert.Equal(t, awserrors.ErrorValidationException, err.Error())
assert.True(t, awserrors.IsErrorCode(err, awserrors.ErrorValidationException))
}
func TestBedrockRuntimeRequest_InvokeModel(t *testing.T) {
+5 -5
View File
@@ -22,17 +22,17 @@ const (
modelBogus = "does.not-exist-v1:0"
)
// TestGetFoundationModel asserts ResourceNotFoundException for an unknown
// model ID. v1 ships no provider-tier entry that resolves unconditionally
// TestGetFoundationModel asserts ValidationException for an unknown model ID,
// as AWS returns. v1 ships no provider-tier entry that resolves unconditionally
// (modelAnthropic is not in the catalog at all — it returns the same
// ResourceNotFoundException as a genuinely unknown ID), so the positive,
// ValidationException as a genuinely unknown ID), so the positive,
// known-model path is covered separately by TestGetFoundationModelSelfHost,
// which is gated on a real weights snapshot being staged.
func TestGetFoundationModel(t *testing.T) {
f := requireBedrockFixture(t)
t.Run("unknown model", func(t *testing.T) {
harness.ExpectError(t, "ResourceNotFoundException", func() error {
harness.ExpectError(t, "ValidationException", func() error {
_, e := f.AWS.Bedrock.GetFoundationModel(&bedrock.GetFoundationModelInput{
ModelIdentifier: aws.String(modelBogus),
})
@@ -41,7 +41,7 @@ func TestGetFoundationModel(t *testing.T) {
})
t.Run("provider-tier model ID is absent from v1", func(t *testing.T) {
harness.ExpectError(t, "ResourceNotFoundException", func() error {
harness.ExpectError(t, "ValidationException", func() error {
_, e := f.AWS.Bedrock.GetFoundationModel(&bedrock.GetFoundationModelInput{
ModelIdentifier: aws.String(modelAnthropic),
})