Thanks to visit codestin.com
Credit goes to github.com

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
26 changes: 14 additions & 12 deletions coderd/x/chatd/chaterror/classify.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,20 +178,22 @@ func Classify(err error) ClassifiedError {
}

retryableHTTP2StreamReset, hasHTTP2StreamReset := classifyHTTP2StreamReset(err)
providerDisabledMatch := containsAny(lower, providerDisabledPatterns...)
// combinedText merges the transport wrapper text with the structured
// provider response body so signal patterns in either are detected.
// AI Bridge writes some failures as plain-text bodies that never reach
// the transport wrapper, so the body can be the only signal regardless
// of the class's nominal status code.
combinedText := lower + "\n" + strings.ToLower(structured.detail)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-2] structured.detail for a text/plain body is only the body's first line, so the new deadline signal misses any credential error whose deadline text is not on line 1. (Netero)

providerErrorResponseMessage cuts plain-text bodies at the first newline (coderd/x/chatd/chaterror/provider_error.go:75). combinedText therefore merges the wrapper with one line of body, not the body.

Verified:

body="line one of the failure\ncreate anthropic client: ...: context deadline exceeded"
  -> kind=generic retryable=true detail="line one of the failure"

I read providerErrorResponseMessage and confirmed the strings.Cut(..., "\n") truncation. The observed AWS SDK error is single-line so the shipped test passes, but AWS credential chain errors are not guaranteed single-line. The signal source and the user-facing detail are currently the same string; match on the full body and keep the first line for Detail to remove the coupling.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to #27918. The first-line truncation coupling between signal matching and detail display is a structural issue that the follow-up refactor will address by decoupling the signal text from the display text.

providerDisabledMatch := containsAny(combinedText, providerDisabledPatterns...)
deadline := errors.Is(err, context.DeadlineExceeded) || strings.Contains(lower, "context deadline exceeded")
overloadedMatch := statusCode == 529 || containsAny(lower, overloadedPatterns...)
// Usage limits do not have a dedicated status code, so provider
// response bodies can be the only reliable signal. Other classes
// already have status-code signals or transport wrapper text.
usageLimitText := lower + "\n" + strings.ToLower(structured.detail)
usageLimitMatch := containsAny(usageLimitText, usageLimitAnyStatusPatterns...) ||
(statusCode != 429 && containsAny(usageLimitText, usageLimitPatterns...))
authStrong := statusCode == 401 || containsAny(lower, authStrongPatterns...)
configMatch := containsAny(lower, configPatterns...)
authWeak := statusCode == 403 || containsAny(lower, authWeakPatterns...)
overloadedMatch := statusCode == 529 || containsAny(combinedText, overloadedPatterns...)
usageLimitMatch := containsAny(combinedText, usageLimitAnyStatusPatterns...) ||
(statusCode != 429 && containsAny(combinedText, usageLimitPatterns...))
authStrong := statusCode == 401 || containsAny(combinedText, authStrongPatterns...)
authWeak := statusCode == 403 || containsAny(combinedText, authWeakPatterns...)
configMatch := containsAny(combinedText, configPatterns...)
rateLimitMatch := statusCode == 429 || containsAny(lower, rateLimitPatterns...)
timeoutPatternMatch := containsAny(lower, timeoutPatterns...)
timeoutPatternMatch := containsAny(combinedText, timeoutPatterns...)
Comment thread
johnstcn marked this conversation as resolved.
if hasHTTP2StreamReset && !retryableHTTP2StreamReset {
// A typed HTTP/2 stream error gives us the reset code. Trust it
// over broader string fallbacks so protocol bugs do not retry.
Expand Down
100 changes: 100 additions & 0 deletions coderd/x/chatd/chaterror/classify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1669,6 +1669,106 @@ func TestClassify_MissingKeyPreClassified(t *testing.T) {
)
}

func TestClassify_BedrockCredentialResolutionDeadline(t *testing.T) {
t.Parallel()

// AIBridge writes credential resolution failures as a plain-text 500.
// The fantasy adapter's Error() returns only the SDK transport wrapper;
// the useful text lives solely in the response body (structured.detail).
// The "resolve aws credentials" pattern in configPatterns matches on
// the body, classifying this as a non-retryable config error instead
// of a retryable generic 500.
classified := chaterror.Classify(testProviderError(
`POST "https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages": 500 Internal Server Error`,
500,
nil,
testPlainDump("text/plain", "create anthropic client: resolve AWS credentials: "+
"failed to refresh cached credentials, no EC2 IMDS role found, "+
"operation error ec2imds: GetMetadata, canceled, context deadline exceeded"),
))

require.Equal(t, codersdk.ChatErrorKindConfig, classified.Kind)
require.False(t, classified.Retryable)
require.Equal(t, 500, classified.StatusCode)
require.Contains(t, classified.Detail, "context deadline exceeded")
}

func TestClassify_BedrockBodyOnlySignals(t *testing.T) {
t.Parallel()

// AIBridge returns plain-text 500 bodies for all client creation
// failures. The fantasy adapter's Error() returns only the transport
// wrapper, so the useful text lives solely in the response body.
// Signal patterns must check combinedText (wrapper + body) for these
// to classify as the correct kind instead of a retryable generic 500.
tests := []struct {
name string
body string
wantKind codersdk.ChatErrorKind
wantRet bool
}{
{
name: "OverloadedInBody",
body: "upstream provider is overloaded, please retry",
wantKind: codersdk.ChatErrorKindOverloaded,
wantRet: true,
},
{
name: "AuthInBody",
body: "unauthorized: the security token included in the request is invalid",
wantKind: codersdk.ChatErrorKindAuth,
wantRet: false,
},
{
name: "ConfigInBody",
body: "create bedrock client: invalid model identifier for this region",
wantKind: codersdk.ChatErrorKindConfig,
wantRet: false,
},
{
name: "TimeoutInBody",
body: "upstream gateway timed out waiting for a response",
wantKind: codersdk.ChatErrorKindTimeout,
wantRet: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

classified := chaterror.Classify(testProviderError(
`POST "https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages": 500 Internal Server Error`,
500,
nil,
testPlainDump("text/plain", tt.body),
))
require.Equal(t, tt.wantKind, classified.Kind, "kind")
require.Equal(t, tt.wantRet, classified.Retryable, "retryable")
require.Equal(t, 500, classified.StatusCode)
})
}
}

func TestClassify_ProviderDisabledBodyOnly(t *testing.T) {
t.Parallel()

// AIBridge writes the provider_disabled sentinel as a plain-text 503
// body. The fantasy adapter's Error() returns only the transport
// wrapper, so the sentinel lives solely in the response body.
// Without checking combinedText, the 503 status code would match the
// timeout rule and classify as retryable instead of non-retryable.
classified := chaterror.Classify(testProviderError(
`POST "https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages": 503 Service Unavailable`,
503,
nil,
testPlainDump("text/plain", `provider_disabled: AI provider "anthropic" is disabled`),
))

require.Equal(t, codersdk.ChatErrorKindProviderDisabled, classified.Kind)
require.False(t, classified.Retryable)
require.Equal(t, 503, classified.StatusCode)
}

func testProviderError(
message string,
statusCode int,
Expand Down
1 change: 1 addition & 0 deletions coderd/x/chatd/chaterror/signals.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ var (
"maximum context length",
"malformed config",
"malformed configuration",
"resolve aws credentials",
}
genericRetryablePatterns = []string{"server error", "internal server error"}
interruptedPatterns = []string{"chat interrupted", "request interrupted", "operation interrupted"}
Expand Down
Loading