From 94895314313af8c3743c742592a73dea43391494 Mon Sep 17 00:00:00 2001 From: alfadb Date: Thu, 26 Feb 2026 23:34:53 +0800 Subject: [PATCH 1/2] 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) --- ...teway_anthropic_apikey_passthrough_test.go | 56 +++++++++---------- backend/internal/service/gateway_service.go | 12 ++-- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go index 41c90690..7bd4cd8a 100644 --- a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go +++ b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go @@ -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) diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 61e8c4c6..8f33678b 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -6015,9 +6015,10 @@ func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context, body, reqModel = normalizeClaudeOAuthRequestBody(body, reqModel, normalizeOpts) } - // Antigravity 账户不支持 count_tokens 转发,直接返回空值 + // Antigravity 账户不支持 count_tokens,返回 404 让客户端 fallback 到本地估算。 + // 返回 nil 避免 handler 层记录为错误,也不设置 ops 上游错误上下文。 if account.Platform == PlatformAntigravity { - c.JSON(http.StatusOK, gin.H{"input_tokens": 0}) + s.countTokensError(c, http.StatusNotFound, "not_found_error", "count_tokens endpoint is not supported for this platform") return nil } @@ -6222,12 +6223,13 @@ func (s *GatewayService) forwardCountTokensAnthropicAPIKeyPassthrough(ctx contex upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody)) upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg) - // 中转站不支持 count_tokens 端点时(404),降级返回空值,客户端会 fallback 到本地估算。 + // 中转站不支持 count_tokens 端点时(404),透传 404 让客户端 fallback 到本地估算。 + // 返回 nil 避免 handler 层记录为错误,也不设置 ops 上游错误上下文。 if resp.StatusCode == http.StatusNotFound { logger.LegacyPrintf("service.gateway", - "[count_tokens] Upstream does not support count_tokens (404), returning fallback: account=%d name=%s msg=%s", + "[count_tokens] Upstream does not support count_tokens (404), passing through: account=%d name=%s msg=%s", account.ID, account.Name, truncateString(upstreamMsg, 512)) - c.JSON(http.StatusOK, gin.H{"input_tokens": 0}) + s.countTokensError(c, http.StatusNotFound, "not_found_error", "count_tokens endpoint is not supported by upstream") return nil } From e6969acb5033fefcfd1740127540b3db47721443 Mon Sep 17 00:00:00 2001 From: alfadb Date: Thu, 26 Feb 2026 23:49:30 +0800 Subject: [PATCH 2/2] fix: address review - fix log wording and add response body assertion in test --- .../service/gateway_anthropic_apikey_passthrough_test.go | 9 ++++++++- backend/internal/service/gateway_service.go | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go index 7bd4cd8a..e3dff6b8 100644 --- a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go +++ b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "encoding/json" "errors" "io" "net/http" @@ -346,9 +347,15 @@ func TestGatewayService_AnthropicAPIKeyPassthrough_CountTokens404PassthroughNotE err := svc.ForwardCountTokens(context.Background(), c, account, parsed) if tt.wantPassthrough { - // 404 透传:返回 nil(不记录为错误),但 HTTP 状态码是 404 + // 返回 nil(不记录为错误),HTTP 状态码 404 + Anthropic 错误体 require.NoError(t, err) require.Equal(t, http.StatusNotFound, rec.Code) + var errResp map[string]any + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &errResp)) + require.Equal(t, "error", errResp["type"]) + errObj, ok := errResp["error"].(map[string]any) + require.True(t, ok) + require.Equal(t, "not_found_error", errObj["type"]) } else { require.Error(t, err) require.Equal(t, tt.statusCode, rec.Code) diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 8f33678b..02a4b012 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -6223,11 +6223,11 @@ func (s *GatewayService) forwardCountTokensAnthropicAPIKeyPassthrough(ctx contex upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody)) upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg) - // 中转站不支持 count_tokens 端点时(404),透传 404 让客户端 fallback 到本地估算。 + // 中转站不支持 count_tokens 端点时(404),返回 404 让客户端 fallback 到本地估算。 // 返回 nil 避免 handler 层记录为错误,也不设置 ops 上游错误上下文。 if resp.StatusCode == http.StatusNotFound { logger.LegacyPrintf("service.gateway", - "[count_tokens] Upstream does not support count_tokens (404), passing through: account=%d name=%s msg=%s", + "[count_tokens] Upstream does not support count_tokens (404), returning 404: account=%d name=%s msg=%s", account.ID, account.Name, truncateString(upstreamMsg, 512)) s.countTokensError(c, http.StatusNotFound, "not_found_error", "count_tokens endpoint is not supported by upstream") return nil