fix(gateway): return 404 instead of fake 200 for unsupported count_tokens endpoint

PR #635 returned HTTP 200 with {"input_tokens": 0} when upstream doesn't
support count_tokens (404). This caused Claude Code CLI to trust the zero
value, believing context uses 0 tokens, so auto-compression never triggers.

Fix: return 404 with proper error body so CLI falls back to its local
tokenizer for accurate estimation. Return nil (not error) to avoid
polluting ops error metrics with expected 404s.

Affected paths:
- Passthrough APIKey accounts: upstream 404 now passed through as 404
- Antigravity accounts: same fix (was also returning fake 200)
This commit is contained in:
alfadb
2026-02-26 23:34:53 +08:00
parent 4ac57b4edf
commit 9489531431
2 changed files with 35 additions and 33 deletions

View File

@@ -262,44 +262,44 @@ func TestGatewayService_AnthropicAPIKeyPassthrough_ForwardCountTokensPreservesBo
require.Empty(t, rec.Header().Get("Set-Cookie"))
}
func TestGatewayService_AnthropicAPIKeyPassthrough_CountTokensFallbackOn404(t *testing.T) {
func TestGatewayService_AnthropicAPIKeyPassthrough_CountTokens404PassthroughNotError(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
statusCode int
respBody string
wantFallback bool
name string
statusCode int
respBody string
wantPassthrough bool
}{
{
name: "404 endpoint not found triggers fallback",
statusCode: http.StatusNotFound,
respBody: `{"error":{"message":"Not found: /v1/messages/count_tokens","type":"not_found_error"}}`,
wantFallback: true,
name: "404 endpoint not found passes through as 404",
statusCode: http.StatusNotFound,
respBody: `{"error":{"message":"Not found: /v1/messages/count_tokens","type":"not_found_error"}}`,
wantPassthrough: true,
},
{
name: "404 generic not found triggers fallback",
statusCode: http.StatusNotFound,
respBody: `{"error":{"message":"resource not found","type":"not_found_error"}}`,
wantFallback: true,
name: "404 generic not found passes through as 404",
statusCode: http.StatusNotFound,
respBody: `{"error":{"message":"resource not found","type":"not_found_error"}}`,
wantPassthrough: true,
},
{
name: "400 Invalid URL does not fallback",
statusCode: http.StatusBadRequest,
respBody: `{"error":{"message":"Invalid URL (POST /v1/messages/count_tokens)","type":"invalid_request_error"}}`,
wantFallback: false,
name: "400 Invalid URL does not passthrough",
statusCode: http.StatusBadRequest,
respBody: `{"error":{"message":"Invalid URL (POST /v1/messages/count_tokens)","type":"invalid_request_error"}}`,
wantPassthrough: false,
},
{
name: "400 model error does not fallback",
statusCode: http.StatusBadRequest,
respBody: `{"error":{"message":"model not found: claude-unknown","type":"invalid_request_error"}}`,
wantFallback: false,
name: "400 model error does not passthrough",
statusCode: http.StatusBadRequest,
respBody: `{"error":{"message":"model not found: claude-unknown","type":"invalid_request_error"}}`,
wantPassthrough: false,
},
{
name: "500 internal error does not fallback",
statusCode: http.StatusInternalServerError,
respBody: `{"error":{"message":"internal error","type":"api_error"}}`,
wantFallback: false,
name: "500 internal error does not passthrough",
statusCode: http.StatusInternalServerError,
respBody: `{"error":{"message":"internal error","type":"api_error"}}`,
wantPassthrough: false,
},
}
@@ -345,10 +345,10 @@ func TestGatewayService_AnthropicAPIKeyPassthrough_CountTokensFallbackOn404(t *t
err := svc.ForwardCountTokens(context.Background(), c, account, parsed)
if tt.wantFallback {
if tt.wantPassthrough {
// 404 透传:返回 nil不记录为错误但 HTTP 状态码是 404
require.NoError(t, err)
require.Equal(t, http.StatusOK, rec.Code)
require.JSONEq(t, `{"input_tokens":0}`, rec.Body.String())
require.Equal(t, http.StatusNotFound, rec.Code)
} else {
require.Error(t, err)
require.Equal(t, tt.statusCode, rec.Code)