mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: move chat error kinds into codersdk (#24955)
Moves the chat error kind taxonomy from `coderd/x/chatd/chaterror` into `codersdk.ChatErrorKind` and types `ChatError.Kind` / `ChatStreamRetry.Kind` so generated TypeScript exposes an SDK-owned union, including `usage_limit`. Backend chat classification now references the SDK constants directly while preserving the existing JSON string values. Keeps chat usage-limit admission failures on their existing 409 response shape. The frontend maps structured usage-limit responses to the SDK-owned `usage_limit` kind, uses generated `TypesGen.ChatErrorKind` directly, and removes the local string union and alias.
This commit is contained in:
@@ -710,7 +710,7 @@ func classifyStartupTimeout(
|
||||
err = errStartupTimeout
|
||||
}
|
||||
return chaterror.WithClassification(err, chaterror.ClassifiedError{
|
||||
Kind: chaterror.KindStartupTimeout,
|
||||
Kind: codersdk.ChatErrorKindStartupTimeout,
|
||||
Provider: provider,
|
||||
Retryable: true,
|
||||
})
|
||||
|
||||
@@ -571,7 +571,7 @@ func TestRun_OnRetryEnrichesProvider(t *testing.T) {
|
||||
require.Equal(t, "received status 429 from upstream", records[0].errMsg)
|
||||
require.Equal(t, chatretry.Delay(0), records[0].delay)
|
||||
require.Equal(t, "openai", records[0].classified.Provider)
|
||||
require.Equal(t, chaterror.KindRateLimit, records[0].classified.Kind)
|
||||
require.Equal(t, codersdk.ChatErrorKindRateLimit, records[0].classified.Kind)
|
||||
require.True(t, records[0].classified.Retryable)
|
||||
require.Equal(t, 429, records[0].classified.StatusCode)
|
||||
require.Equal(
|
||||
@@ -633,7 +633,7 @@ func TestStartupGuard_DisarmPreservesPermanentError(t *testing.T) {
|
||||
"openai",
|
||||
xerrors.New("invalid model"),
|
||||
))
|
||||
require.Equal(t, chaterror.KindConfig, classified.Kind)
|
||||
require.Equal(t, codersdk.ChatErrorKindConfig, classified.Kind)
|
||||
require.False(t, classified.Retryable)
|
||||
require.Nil(t, context.Cause(attemptCtx))
|
||||
}
|
||||
@@ -700,7 +700,7 @@ func TestRun_RetriesStartupTimeoutWhileOpeningStream(t *testing.T) {
|
||||
require.NoError(t, awaitRunResult(ctx, t, done))
|
||||
require.Equal(t, 2, attempts)
|
||||
require.Len(t, retries, 1)
|
||||
require.Equal(t, chaterror.KindStartupTimeout, retries[0].Kind)
|
||||
require.Equal(t, codersdk.ChatErrorKindStartupTimeout, retries[0].Kind)
|
||||
require.True(t, retries[0].Retryable)
|
||||
require.Equal(t, "openai", retries[0].Provider)
|
||||
require.Equal(
|
||||
@@ -788,7 +788,7 @@ func TestRun_HTTP2TransportErrorClassifiedAsRetryableTimeout(t *testing.T) {
|
||||
require.NoError(t, awaitRunResult(ctx, t, done))
|
||||
require.Equal(t, 2, attempts)
|
||||
require.Len(t, retries, 1)
|
||||
require.Equal(t, chaterror.KindTimeout, retries[0].Kind, "Kind")
|
||||
require.Equal(t, codersdk.ChatErrorKindTimeout, retries[0].Kind, "Kind")
|
||||
require.True(t, retries[0].Retryable, "Retryable")
|
||||
require.Equal(t, provider, retries[0].Provider, "Provider")
|
||||
})
|
||||
@@ -862,7 +862,7 @@ func TestRun_RetriesStartupTimeoutBeforeFirstPart(t *testing.T) {
|
||||
require.NoError(t, awaitRunResult(ctx, t, done))
|
||||
require.Equal(t, 2, attempts)
|
||||
require.Len(t, retries, 1)
|
||||
require.Equal(t, chaterror.KindStartupTimeout, retries[0].Kind)
|
||||
require.Equal(t, codersdk.ChatErrorKindStartupTimeout, retries[0].Kind)
|
||||
require.True(t, retries[0].Retryable)
|
||||
require.Equal(t, "openai", retries[0].Provider)
|
||||
require.Equal(
|
||||
@@ -1077,7 +1077,7 @@ func TestRun_RetriesStartupTimeoutWhenStreamClosesSilently(t *testing.T) {
|
||||
require.NoError(t, awaitRunResult(ctx, t, done))
|
||||
require.Equal(t, 2, attempts)
|
||||
require.Len(t, retries, 1)
|
||||
require.Equal(t, chaterror.KindStartupTimeout, retries[0].Kind)
|
||||
require.Equal(t, codersdk.ChatErrorKindStartupTimeout, retries[0].Kind)
|
||||
require.True(t, retries[0].Retryable)
|
||||
require.Equal(t, "openai", retries[0].Provider)
|
||||
require.Equal(
|
||||
|
||||
@@ -145,7 +145,7 @@ func (m *Metrics) RecordStreamRetry(provider, model string, classified chaterror
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
m.StreamRetriesTotal.WithLabelValues(provider, model, classified.Kind).Inc()
|
||||
m.StreamRetriesTotal.WithLabelValues(provider, model, string(classified.Kind)).Inc()
|
||||
}
|
||||
|
||||
// RecordToolError increments tool_errors_total for the given
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/x/chatd/chatloop"
|
||||
"github.com/coder/coder/v2/coderd/x/chatd/chatretry"
|
||||
"github.com/coder/coder/v2/coderd/x/chatd/chattest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func TestNewMetrics_RegistersAllMetrics(t *testing.T) {
|
||||
@@ -33,7 +34,7 @@ func TestNewMetrics_RegistersAllMetrics(t *testing.T) {
|
||||
m.PromptSizeBytes.WithLabelValues("anthropic", "claude-sonnet-4-5")
|
||||
m.TTFTSeconds.WithLabelValues("anthropic", "claude-sonnet-4-5")
|
||||
m.StepsTotal.WithLabelValues("anthropic", "claude-sonnet-4-5")
|
||||
m.StreamRetriesTotal.WithLabelValues("anthropic", "claude-sonnet-4-5", chaterror.KindTimeout)
|
||||
m.StreamRetriesTotal.WithLabelValues("anthropic", "claude-sonnet-4-5", string(codersdk.ChatErrorKindTimeout))
|
||||
// StreamBufferDroppedTotal is a plain Counter, so it's always present
|
||||
// in Gather output once registered; no exerciser call is
|
||||
// needed.
|
||||
@@ -87,14 +88,14 @@ func TestNopMetrics_DoesNotPanic(t *testing.T) {
|
||||
m.CompactionTotal.WithLabelValues("openai", "gpt-5", "error").Inc()
|
||||
m.CompactionTotal.WithLabelValues("google", "gemini-2.5-pro", "timeout").Inc()
|
||||
m.StepsTotal.WithLabelValues("anthropic", "claude-sonnet-4-5").Inc()
|
||||
m.StreamRetriesTotal.WithLabelValues("anthropic", "claude-sonnet-4-5", chaterror.KindTimeout).Inc()
|
||||
m.StreamRetriesTotal.WithLabelValues("anthropic", "claude-sonnet-4-5", string(codersdk.ChatErrorKindTimeout)).Inc()
|
||||
m.StreamBufferDroppedTotal.Inc()
|
||||
|
||||
// Nil-receiver guard for RecordStreamRetry and
|
||||
// RecordStreamBufferDropped mirrors the existing RecordCompaction nil
|
||||
// guard.
|
||||
var nilMetrics *chatloop.Metrics
|
||||
nilMetrics.RecordStreamRetry("anthropic", "claude-sonnet-4-5", chaterror.ClassifiedError{Kind: chaterror.KindTimeout})
|
||||
nilMetrics.RecordStreamRetry("anthropic", "claude-sonnet-4-5", chaterror.ClassifiedError{Kind: codersdk.ChatErrorKindTimeout})
|
||||
nilMetrics.RecordStreamBufferDropped()
|
||||
nilMetrics.RecordToolError("anthropic", "claude-sonnet-4-5", "test")
|
||||
}
|
||||
@@ -279,21 +280,21 @@ func TestRecordCompaction(t *testing.T) {
|
||||
func TestRecordStreamRetry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// One row per chaterror.Kind* constant. Production callers always
|
||||
// One row per ChatErrorKind constant. Production callers always
|
||||
// reach RecordStreamRetry through chaterror.Classify, which
|
||||
// guarantees Kind is non-empty, so no empty-string case is
|
||||
// needed.
|
||||
tests := []struct {
|
||||
name string
|
||||
kind string
|
||||
kind codersdk.ChatErrorKind
|
||||
}{
|
||||
{name: "overloaded", kind: chaterror.KindOverloaded},
|
||||
{name: "rate_limit", kind: chaterror.KindRateLimit},
|
||||
{name: "timeout", kind: chaterror.KindTimeout},
|
||||
{name: "startup_timeout", kind: chaterror.KindStartupTimeout},
|
||||
{name: "auth", kind: chaterror.KindAuth},
|
||||
{name: "config", kind: chaterror.KindConfig},
|
||||
{name: "generic", kind: chaterror.KindGeneric},
|
||||
{name: "overloaded", kind: codersdk.ChatErrorKindOverloaded},
|
||||
{name: "rate_limit", kind: codersdk.ChatErrorKindRateLimit},
|
||||
{name: "timeout", kind: codersdk.ChatErrorKindTimeout},
|
||||
{name: "startup_timeout", kind: codersdk.ChatErrorKindStartupTimeout},
|
||||
{name: "auth", kind: codersdk.ChatErrorKindAuth},
|
||||
{name: "config", kind: codersdk.ChatErrorKindConfig},
|
||||
{name: "generic", kind: codersdk.ChatErrorKindGeneric},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -309,7 +310,7 @@ func TestRecordStreamRetry(t *testing.T) {
|
||||
requireCounter(t, reg, "coderd_chatd_stream_retries_total", 1, map[string]string{
|
||||
"provider": "test-provider",
|
||||
"model": "test-model",
|
||||
"kind": tt.kind,
|
||||
"kind": string(tt.kind),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -558,14 +559,14 @@ func TestRun_StreamRetry_RecordsMetric(t *testing.T) {
|
||||
// Back-compat: OnRetry still fires with classified error.
|
||||
require.Len(t, retries, 1)
|
||||
assert.Equal(t, 1, retries[0].attempt)
|
||||
assert.Equal(t, chaterror.KindRateLimit, retries[0].classified.Kind)
|
||||
assert.Equal(t, codersdk.ChatErrorKindRateLimit, retries[0].classified.Kind)
|
||||
assert.Equal(t, "test-provider", retries[0].classified.Provider)
|
||||
|
||||
// Metric assertion.
|
||||
requireCounter(t, reg, "coderd_chatd_stream_retries_total", 1, map[string]string{
|
||||
"provider": "test-provider",
|
||||
"model": "test-model",
|
||||
"kind": chaterror.KindRateLimit,
|
||||
"kind": string(codersdk.ChatErrorKindRateLimit),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user