diff --git a/providers/bedrock/provider_test.go b/providers/bedrock/provider_test.go index cc692ad3..cec6b7ea 100644 --- a/providers/bedrock/provider_test.go +++ b/providers/bedrock/provider_test.go @@ -470,6 +470,42 @@ func TestRequestMapper_ToolDefinitions(t *testing.T) { assert.True(t, ok) } +func TestRequestMapper_EmptyToolDescription(t *testing.T) { + t.Parallel() + + cfg := &Config{ + ModelName: "claude-sonnet-4-6", + setOptions: make(map[string]bool), + } + + mapper := NewRequestMapper(cfg) + + schema := json.RawMessage(`{"type":"object"}`) + + req := &llm.Request{ + Messages: []llm.Message{ + llm.NewMessage(llm.RoleUser, llm.NewTextPart("hi")), + }, + Tools: []llm.ToolDefinition{ + {Name: "search", Description: "Search the web", Parameters: schema}, + {Name: "subagent_a", Description: "", Parameters: schema}, + {Name: "summarize", Description: "Summarize text", Parameters: schema}, + {Name: "subagent_b", Description: "", Parameters: schema}, + }, + ToolChoice: &llm.ToolChoice{Type: llm.ToolChoiceAuto}, + } + + _, err := mapper.ToConverseInput(req) + require.Error(t, err) + require.ErrorIs(t, err, llm.ErrRequestMapping) + // All offenders surfaced in a single error so callers can fix in one pass. + assert.Contains(t, err.Error(), "subagent_a") + assert.Contains(t, err.Error(), "subagent_b") + // Tools with valid descriptions are not flagged. + assert.NotContains(t, err.Error(), "missing on: search") + assert.NotContains(t, err.Error(), "summarize,") +} + func TestRequestMapper_ToolChoiceSpecific(t *testing.T) { t.Parallel() diff --git a/providers/bedrock/request_mapper.go b/providers/bedrock/request_mapper.go index a0aa9347..1cdbdc16 100644 --- a/providers/bedrock/request_mapper.go +++ b/providers/bedrock/request_mapper.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" @@ -314,6 +315,22 @@ func (rm *RequestMapper) mapToolResultBlock(resp *llm.ToolResponse) types.ToolRe // mapToolConfig converts tool definitions and choice to Bedrock ToolConfiguration. func (rm *RequestMapper) mapToolConfig(tools []llm.ToolDefinition, choice *llm.ToolChoice) (*types.ToolConfiguration, error) { + // Bedrock Converse rejects any tool whose description is the empty + // string with an opaque ValidationException ("Member must have length + // greater than or equal to 1"). Validate upfront and surface every + // offender at once so callers can fix all of them in one pass. + var missingDesc []string + + for _, tool := range tools { + if tool.Description == "" { + missingDesc = append(missingDesc, tool.Name) + } + } + + if len(missingDesc) > 0 { + return nil, fmt.Errorf("bedrock: every tool must have a non-empty description; missing on: %s", strings.Join(missingDesc, ", ")) + } + apiTools := make([]types.Tool, 0, len(tools)) for _, tool := range tools {