From b7c2c59931184b8e2648549f61221d2bc727e037 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 22 Apr 2026 15:14:31 +0200 Subject: [PATCH] fix(coderd/x/chatd/chatdebug): allow Anthropic per-modality ratelimit headers (#24592) Previously, Anthropic's per-modality, Priority Tier, and fast-mode rate-limit headers (`Anthropic-Ratelimit-Input-Tokens-*`, `Anthropic-Ratelimit-Output-Tokens-*`, `Anthropic-Priority-Input-Tokens-*`, `Anthropic-Priority-Output-Tokens-*`, `Anthropic-Fast-Input-Tokens-*`, and `Anthropic-Fast-Output-Tokens-*`) were shown as `[REDACTED]` in the Debug panel because they contain `"token"` in the name and fell through the generic credential filter. Add them to the allowlist in `coderd/x/chatd/chatdebug/redaction.go` alongside the existing `Anthropic-Ratelimit-Tokens-*` entries so the limits/remaining/reset values surface in the raw response view. --- coderd/x/chatd/chatdebug/redaction.go | 49 ++++++++++++++++------ coderd/x/chatd/chatdebug/redaction_test.go | 48 ++++++++++++++++++--- 2 files changed, 79 insertions(+), 18 deletions(-) diff --git a/coderd/x/chatd/chatdebug/redaction.go b/coderd/x/chatd/chatdebug/redaction.go index 6ac8d6d71b..fc4677c710 100644 --- a/coderd/x/chatd/chatdebug/redaction.go +++ b/coderd/x/chatd/chatdebug/redaction.go @@ -162,19 +162,44 @@ func consumeJSONEOF(decoder *json.Decoder) error { return err } +// safeRateLimitHeaderNames lists rate-limit headers that contain +// "token" in the name but carry numeric usage counters, not +// credentials. They are checked in isSensitiveName before the +// generic "token" substring match so they pass through unredacted. +// Add new entries here when a provider introduces a rate-limit +// header family containing "token" (e.g. Anthropic's per-modality, +// Priority Tier, or fast-mode headers). var safeRateLimitHeaderNames = map[string]struct{}{ - "anthropic-ratelimit-requests-limit": {}, - "anthropic-ratelimit-requests-remaining": {}, - "anthropic-ratelimit-requests-reset": {}, - "anthropic-ratelimit-tokens-limit": {}, - "anthropic-ratelimit-tokens-remaining": {}, - "anthropic-ratelimit-tokens-reset": {}, - "x-ratelimit-limit-requests": {}, - "x-ratelimit-limit-tokens": {}, - "x-ratelimit-remaining-requests": {}, - "x-ratelimit-remaining-tokens": {}, - "x-ratelimit-reset-requests": {}, - "x-ratelimit-reset-tokens": {}, + "anthropic-ratelimit-requests-limit": {}, + "anthropic-ratelimit-requests-remaining": {}, + "anthropic-ratelimit-requests-reset": {}, + "anthropic-ratelimit-tokens-limit": {}, + "anthropic-ratelimit-tokens-remaining": {}, + "anthropic-ratelimit-tokens-reset": {}, + "anthropic-ratelimit-input-tokens-limit": {}, + "anthropic-ratelimit-input-tokens-remaining": {}, + "anthropic-ratelimit-input-tokens-reset": {}, + "anthropic-ratelimit-output-tokens-limit": {}, + "anthropic-ratelimit-output-tokens-remaining": {}, + "anthropic-ratelimit-output-tokens-reset": {}, + "anthropic-priority-input-tokens-limit": {}, + "anthropic-priority-input-tokens-remaining": {}, + "anthropic-priority-input-tokens-reset": {}, + "anthropic-priority-output-tokens-limit": {}, + "anthropic-priority-output-tokens-remaining": {}, + "anthropic-priority-output-tokens-reset": {}, + "anthropic-fast-input-tokens-limit": {}, + "anthropic-fast-input-tokens-remaining": {}, + "anthropic-fast-input-tokens-reset": {}, + "anthropic-fast-output-tokens-limit": {}, + "anthropic-fast-output-tokens-remaining": {}, + "anthropic-fast-output-tokens-reset": {}, + "x-ratelimit-limit-requests": {}, + "x-ratelimit-limit-tokens": {}, + "x-ratelimit-remaining-requests": {}, + "x-ratelimit-remaining-tokens": {}, + "x-ratelimit-reset-requests": {}, + "x-ratelimit-reset-tokens": {}, } // isSensitiveName reports whether a name (header or query parameter) diff --git a/coderd/x/chatd/chatdebug/redaction_test.go b/coderd/x/chatd/chatdebug/redaction_test.go index c60d72a802..9fefe26118 100644 --- a/coderd/x/chatd/chatdebug/redaction_test.go +++ b/coderd/x/chatd/chatdebug/redaction_test.go @@ -88,18 +88,54 @@ func TestRedactHeaders(t *testing.T) { t.Parallel() headers := http.Header{ - "Anthropic-Ratelimit-Tokens-Limit": {"1000000"}, - "Anthropic-Ratelimit-Tokens-Remaining": {"999000"}, - "Anthropic-Ratelimit-Tokens-Reset": {"2026-03-31T08:55:26Z"}, - "X-RateLimit-Limit-Tokens": {"120000"}, - "X-RateLimit-Remaining-Tokens": {"119500"}, - "X-RateLimit-Reset-Tokens": {"12ms"}, + "Anthropic-Ratelimit-Tokens-Limit": {"1000000"}, + "Anthropic-Ratelimit-Tokens-Remaining": {"999000"}, + "Anthropic-Ratelimit-Tokens-Reset": {"2026-03-31T08:55:26Z"}, + "Anthropic-Ratelimit-Input-Tokens-Limit": {"200000"}, + "Anthropic-Ratelimit-Input-Tokens-Remaining": {"199000"}, + "Anthropic-Ratelimit-Input-Tokens-Reset": {"2026-03-31T08:55:26Z"}, + "Anthropic-Ratelimit-Output-Tokens-Limit": {"80000"}, + "Anthropic-Ratelimit-Output-Tokens-Remaining": {"79500"}, + "Anthropic-Ratelimit-Output-Tokens-Reset": {"2026-03-31T08:55:26Z"}, + "Anthropic-Priority-Input-Tokens-Limit": {"10000"}, + "Anthropic-Priority-Input-Tokens-Remaining": {"9618"}, + "Anthropic-Priority-Input-Tokens-Reset": {"2026-03-31T08:55:26Z"}, + "Anthropic-Priority-Output-Tokens-Limit": {"10000"}, + "Anthropic-Priority-Output-Tokens-Remaining": {"6000"}, + "Anthropic-Priority-Output-Tokens-Reset": {"2026-03-31T08:55:26Z"}, + "Anthropic-Fast-Input-Tokens-Limit": {"50000"}, + "Anthropic-Fast-Input-Tokens-Remaining": {"49000"}, + "Anthropic-Fast-Input-Tokens-Reset": {"2026-03-31T08:55:26Z"}, + "Anthropic-Fast-Output-Tokens-Limit": {"25000"}, + "Anthropic-Fast-Output-Tokens-Remaining": {"24000"}, + "Anthropic-Fast-Output-Tokens-Reset": {"2026-03-31T08:55:26Z"}, + "X-RateLimit-Limit-Tokens": {"120000"}, + "X-RateLimit-Remaining-Tokens": {"119500"}, + "X-RateLimit-Reset-Tokens": {"12ms"}, } redacted := chatdebug.RedactHeaders(headers) require.Equal(t, "1000000", redacted["Anthropic-Ratelimit-Tokens-Limit"]) require.Equal(t, "999000", redacted["Anthropic-Ratelimit-Tokens-Remaining"]) require.Equal(t, "2026-03-31T08:55:26Z", redacted["Anthropic-Ratelimit-Tokens-Reset"]) + require.Equal(t, "200000", redacted["Anthropic-Ratelimit-Input-Tokens-Limit"]) + require.Equal(t, "199000", redacted["Anthropic-Ratelimit-Input-Tokens-Remaining"]) + require.Equal(t, "2026-03-31T08:55:26Z", redacted["Anthropic-Ratelimit-Input-Tokens-Reset"]) + require.Equal(t, "80000", redacted["Anthropic-Ratelimit-Output-Tokens-Limit"]) + require.Equal(t, "79500", redacted["Anthropic-Ratelimit-Output-Tokens-Remaining"]) + require.Equal(t, "2026-03-31T08:55:26Z", redacted["Anthropic-Ratelimit-Output-Tokens-Reset"]) + require.Equal(t, "10000", redacted["Anthropic-Priority-Input-Tokens-Limit"]) + require.Equal(t, "9618", redacted["Anthropic-Priority-Input-Tokens-Remaining"]) + require.Equal(t, "2026-03-31T08:55:26Z", redacted["Anthropic-Priority-Input-Tokens-Reset"]) + require.Equal(t, "10000", redacted["Anthropic-Priority-Output-Tokens-Limit"]) + require.Equal(t, "6000", redacted["Anthropic-Priority-Output-Tokens-Remaining"]) + require.Equal(t, "2026-03-31T08:55:26Z", redacted["Anthropic-Priority-Output-Tokens-Reset"]) + require.Equal(t, "50000", redacted["Anthropic-Fast-Input-Tokens-Limit"]) + require.Equal(t, "49000", redacted["Anthropic-Fast-Input-Tokens-Remaining"]) + require.Equal(t, "2026-03-31T08:55:26Z", redacted["Anthropic-Fast-Input-Tokens-Reset"]) + require.Equal(t, "25000", redacted["Anthropic-Fast-Output-Tokens-Limit"]) + require.Equal(t, "24000", redacted["Anthropic-Fast-Output-Tokens-Remaining"]) + require.Equal(t, "2026-03-31T08:55:26Z", redacted["Anthropic-Fast-Output-Tokens-Reset"]) require.Equal(t, "120000", redacted["X-RateLimit-Limit-Tokens"]) require.Equal(t, "119500", redacted["X-RateLimit-Remaining-Tokens"]) require.Equal(t, "12ms", redacted["X-RateLimit-Reset-Tokens"])