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 }