mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Anthropic HTTP 400 responses (e.g. "image exceeds 5 MB maximum") were collapsed in the chat UI to the generic headline "Anthropic returned an unexpected error (HTTP 400)." with no actionable detail — the upstream message survived to the processor log but was dropped before reaching the client. Add a new optional `Detail` field on `codersdk.ChatStreamError` that carries the upstream provider message alongside the existing normalized headline. The backend extracts `error.message` from `fantasy.ProviderError.ResponseBody` (the JSON envelope shared by Anthropic and OpenAI), falls back to the trimmed provider message when the body is absent or unparseable, and caps the result at 500 runes. The frontend threads `Detail` through `useChatStore`, `liveStatusModel`, and `ChatStatusCallout`, rendering it as a muted secondary line inside the existing `AlertDescription`. Before: <img width="1552" height="185" alt="image" src="https://github.com/user-attachments/assets/524b588e-3cee-4fad-bc15-6bf3aec0899d" /> After: <img width="814" height="173" alt="image" src="https://github.com/user-attachments/assets/eae82a89-3ac1-4a33-8d18-ef9f77263d89" /> ## Persistence `Detail` is **not** persisted — it disappears on refresh. Persisting it would require a DB change (today `chats.last_error` is a single nullable `TEXT` column), and the shape of persisted chat errors is worth a more deliberate rethink — e.g. promoting `last_error` to `JSONB` so we can also retain structured fields like `kind`, `statusCode`, `provider`, and `retryable` instead of only the normalized headline string. That's a bigger design discussion than this PR should carry. In the meantime, seeing the upstream error reason *immediately on failure* is already a large UX improvement over the status quo, and this PR gets us there without prejudicing the eventual persistence design. Tracking persistence in CODAGT-239. Closes CODAGT-235
74 lines
2.1 KiB
Go
74 lines
2.1 KiB
Go
package chaterror_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chaterror"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
func TestStreamErrorPayloadUsesNormalizedClassification(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
classified := chaterror.Classify(
|
|
xerrors.New("azure openai received status 429 from upstream"),
|
|
)
|
|
payload := chaterror.StreamErrorPayload(classified)
|
|
|
|
require.Equal(t, &codersdk.ChatStreamError{
|
|
Message: "Azure OpenAI is rate limiting requests (HTTP 429).",
|
|
Kind: chaterror.KindRateLimit,
|
|
Provider: "azure",
|
|
Retryable: true,
|
|
StatusCode: 429,
|
|
}, payload)
|
|
}
|
|
|
|
func TestStreamErrorPayloadIncludesProviderDetail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := chaterror.StreamErrorPayload(chaterror.Classify(testProviderError(
|
|
"",
|
|
400,
|
|
nil,
|
|
testProviderResponseDump(`{"error":{"message":"Image exceeds 5 MB maximum."}}`),
|
|
)))
|
|
|
|
require.Equal(t, "Image exceeds 5 MB maximum.", payload.Detail)
|
|
}
|
|
|
|
func TestStreamErrorPayloadNilForEmptyClassification(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.Nil(t, chaterror.StreamErrorPayload(chaterror.ClassifiedError{}))
|
|
}
|
|
|
|
func TestStreamRetryPayloadUsesNormalizedClassification(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
delay := 3 * time.Second
|
|
startedAt := time.Now()
|
|
payload := chaterror.StreamRetryPayload(2, delay, chaterror.ClassifiedError{
|
|
Message: "OpenAI returned an unexpected error (HTTP 503).",
|
|
Kind: chaterror.KindGeneric,
|
|
Provider: "openai",
|
|
Retryable: true,
|
|
StatusCode: 503,
|
|
})
|
|
|
|
require.NotNil(t, payload)
|
|
require.Equal(t, 2, payload.Attempt)
|
|
require.Equal(t, delay.Milliseconds(), payload.DelayMs)
|
|
// Retry messages omit the HTTP status code; the status code is
|
|
// surfaced separately in the payload's StatusCode field.
|
|
require.Equal(t, "OpenAI returned an unexpected error.", payload.Error)
|
|
require.Equal(t, chaterror.KindGeneric, payload.Kind)
|
|
require.Equal(t, "openai", payload.Provider)
|
|
require.Equal(t, 503, payload.StatusCode)
|
|
require.WithinDuration(t, startedAt.Add(delay), payload.RetryingAt, time.Second)
|
|
}
|