Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions pkg/providers/bedrock/provider_bedrock.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ func (p *Provider) Chat(
// Call Bedrock Converse API
output, err := p.client.Converse(ctx, input)
if err != nil {
// Check for SSO token expiration errors and provide actionable guidance
if isSSOTokenError(err) {
return nil, fmt.Errorf("bedrock converse: AWS credentials may have expired. If using AWS SSO, run 'aws sso login' to refresh: %w", err)
}
Comment thread
loafoe marked this conversation as resolved.
return nil, fmt.Errorf("bedrock converse: %w", err)
}

Expand Down Expand Up @@ -580,3 +584,30 @@ func parseResponse(output *bedrockruntime.ConverseOutput) (*LLMResponse, error)
Usage: usage,
}, nil
}

// isSSOTokenError checks if the error is related to expired or invalid AWS SSO tokens.
// This helps provide actionable guidance when SSO credentials need to be refreshed.
// Only matches SSO-specific error patterns to avoid misclassifying other AWS credential errors.
func isSSOTokenError(err error) bool {
if err == nil {
return false
}
lower := strings.ToLower(err.Error())

// Check for specific SSO token expiration/refresh-related error patterns (case-insensitive)
// Avoid matching generic patterns that could match non-SSO AWS errors (e.g., STS ExpiredToken)
if strings.Contains(lower, "refresh cached sso token") {
return true
}
if strings.Contains(lower, "read cached sso token") {
return true
}
if strings.Contains(lower, "sso oidc") {
return true
}
if strings.Contains(lower, "invalidgrantexception") {
return true
}

return false
}
62 changes: 62 additions & 0 deletions pkg/providers/bedrock/provider_bedrock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package bedrock

import (
"fmt"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
Expand Down Expand Up @@ -539,3 +540,64 @@ func TestParseResponse_ToolCallWithNilInput(t *testing.T) {
assert.NotNil(t, resp.ToolCalls[0].Arguments)
assert.Empty(t, resp.ToolCalls[0].Arguments)
}

func TestIsSSOTokenError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "nil error",
err: nil,
expected: false,
},
{
name: "generic error",
err: fmt.Errorf("connection refused"),
expected: false,
},
{
name: "SSO config error not expiration",
err: fmt.Errorf("failed to load SSO profile: invalid SSO session"),
expected: false,
},
{
name: "STS ExpiredToken error",
err: fmt.Errorf("ExpiredToken: The security token included in the request is expired"),
expected: false,
},
{
name: "SSO token refresh error",
err: fmt.Errorf("refresh cached SSO token failed"),
expected: true,
},
{
name: "InvalidGrantException",
err: fmt.Errorf("operation error SSO OIDC: CreateToken, InvalidGrantException"),
expected: true,
},
{
name: "SSO OIDC error",
err: fmt.Errorf("operation error SSO OIDC: CreateToken, failed"),
expected: true,
},
{
Comment thread
loafoe marked this conversation as resolved.
name: "full SSO error message",
err: fmt.Errorf("get identity: get credentials: failed to refresh cached credentials, refresh cached SSO token failed, unable to refresh SSO token"),
expected: true,
},
{
name: "SSO token file missing",
err: fmt.Errorf("get identity: get credentials: failed to refresh cached credentials, failed to read cached SSO token file, open ~/.aws/sso/cache/abc123.json: no such file or directory"),
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isSSOTokenError(tt.err)
assert.Equal(t, tt.expected, result)
})
}
}
Loading