mirror of
https://github.com/coder/coder.git
synced 2026-09-24 06:47:27 +08:00
Categorises the terminal error of a failed interception and persists it on the interception record, then surfaces it on the AI Gateway API. - Categorise into an enum (`bad_request`, `unauthorized`, `rate_limited`, `overloaded`, `server_error`, `unknown`), unwrapping the ResponseError envelope, the upstream Anthropic/OpenAI SDK errors, and key-pool exhaustion so blocking and streaming paths agree. - Thread the type and raw message through the recorder dRPC into the `aibridge_interceptions` row (optional proto fields; NULL on success). - Expose the error on the AI Gateway thread API from the root interception. *This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.*
69 lines
2.4 KiB
Go
69 lines
2.4 KiB
Go
package integrationtest
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tidwall/sjson"
|
|
|
|
"github.com/coder/coder/v2/aibridge/config"
|
|
"github.com/coder/coder/v2/aibridge/fixtures"
|
|
"github.com/coder/coder/v2/aibridge/internal/testutil"
|
|
"github.com/coder/coder/v2/aibridge/recorder"
|
|
)
|
|
|
|
// TestInterceptionUpstreamErrorRecorded verifies that a failed interception
|
|
// records a categorized upstream error on the ended record.
|
|
//
|
|
// The default test provider is centralized (backed by a single-key pool), so a
|
|
// 401 exhausts the pool. Both blocking and streaming interceptors preserve the
|
|
// *keypool.Error so the cause is categorized as "unauthorized".
|
|
func TestInterceptionUpstreamErrorRecorded(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
provider string
|
|
fixture []byte
|
|
path string
|
|
streaming bool
|
|
wantType recorder.ErrorType
|
|
}{
|
|
{"anthropic_blocking", config.ProviderAnthropic, fixtures.AntSimple, pathAnthropicMessages, false, recorder.ErrorTypeUnauthorized},
|
|
{"anthropic_streaming", config.ProviderAnthropic, fixtures.AntSimple, pathAnthropicMessages, true, recorder.ErrorTypeUnauthorized},
|
|
{"openai_blocking", config.ProviderOpenAI, fixtures.OaiChatSimple, pathOpenAIChatCompletions, false, recorder.ErrorTypeUnauthorized},
|
|
{"openai_streaming", config.ProviderOpenAI, fixtures.OaiChatSimple, pathOpenAIChatCompletions, true, recorder.ErrorTypeUnauthorized},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fix := fixtures.Parse(t, tc.fixture)
|
|
upstream := testutil.NewMockUpstream(t.Context(), t,
|
|
testutil.NewErrorResponse(http.StatusUnauthorized, ""),
|
|
)
|
|
upstream.AllowOverflow = true
|
|
|
|
bridgeServer := newBridgeTestServer(t.Context(), t, upstream.URL, withProvider(tc.provider))
|
|
|
|
reqBody, err := sjson.SetBytes(fix.Request(), "stream", tc.streaming)
|
|
require.NoError(t, err)
|
|
|
|
resp, err := bridgeServer.makeRequest(t, http.MethodPost, tc.path, reqBody)
|
|
require.NoError(t, err)
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
require.NoError(t, resp.Body.Close())
|
|
|
|
intcs := bridgeServer.Recorder.RecordedInterceptions()
|
|
require.Len(t, intcs, 1)
|
|
ended := bridgeServer.Recorder.RecordedInterceptionEnd(intcs[0].ID)
|
|
require.NotNil(t, ended, "interception should be ended")
|
|
require.Equal(t, tc.wantType, ended.ErrorType)
|
|
require.NotEmpty(t, ended.ErrorMessage)
|
|
})
|
|
}
|
|
}
|