From 06216aad53d8d09416c01e86fc2b6d73496011d1 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Mon, 5 Jan 2026 00:56:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(backend):=20=E4=BF=AE=E5=A4=8D=20CI=20?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复内容: 1. 修复 6 个 golangci-lint 错误 - 3 个 errcheck 错误:在 gateway_request_test.go 中添加类型断言检查 - 3 个 gofmt 格式化问题:修复代码格式 2. 修复 API 契约测试失败 - 在测试中添加缺失的字段:enable_identity_patch 和 identity_patch_prompt 所有测试和 linter 检查现已通过。 --- backend/internal/server/api_contract_test.go | 4 +- .../antigravity_gateway_service_test.go | 4 +- backend/internal/service/gateway_request.go | 2 +- .../internal/service/gateway_request_test.go | 57 +++++-- backend/internal/service/gateway_service.go | 152 +++++++++--------- 5 files changed, 122 insertions(+), 97 deletions(-) diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 8a469661..d7ab1ceb 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -313,7 +313,9 @@ func TestAPIContracts(t *testing.T) { "fallback_model_anthropic": "claude-3-5-sonnet-20241022", "fallback_model_antigravity": "gemini-2.5-pro", "fallback_model_gemini": "gemini-2.5-pro", - "fallback_model_openai": "gpt-4o" + "fallback_model_openai": "gpt-4o", + "enable_identity_patch": true, + "identity_patch_prompt": "" } }`, }, diff --git a/backend/internal/service/antigravity_gateway_service_test.go b/backend/internal/service/antigravity_gateway_service_test.go index c3d9ce4c..05ad9bbd 100644 --- a/backend/internal/service/antigravity_gateway_service_test.go +++ b/backend/internal/service/antigravity_gateway_service_test.go @@ -17,14 +17,14 @@ func TestStripSignatureSensitiveBlocksFromClaudeRequest(t *testing.T) { }, Messages: []antigravity.ClaudeMessage{ { - Role: "assistant", + Role: "assistant", Content: json.RawMessage(`[ {"type":"thinking","thinking":"secret plan","signature":""}, {"type":"tool_use","id":"t1","name":"Bash","input":{"command":"ls"}} ]`), }, { - Role: "user", + Role: "user", Content: json.RawMessage(`[ {"type":"tool_result","tool_use_id":"t1","content":"ok","is_error":false}, {"type":"redacted_thinking","data":"..."} diff --git a/backend/internal/service/gateway_request.go b/backend/internal/service/gateway_request.go index 8e94dad2..b385d2dc 100644 --- a/backend/internal/service/gateway_request.go +++ b/backend/internal/service/gateway_request.go @@ -91,7 +91,7 @@ func FilterThinkingBlocks(body []byte) []byte { // - Anthropic extended thinking has a structural constraint: when top-level `thinking` is enabled and the // final message is an assistant prefill, the assistant content must start with a thinking block. // - If we remove thinking blocks but keep top-level `thinking` enabled, we can trigger: -// "Expected `thinking` or `redacted_thinking`, but found `text`" +// "Expected `thinking` or `redacted_thinking`, but found `text`" // // Strategy (B: preserve content as text): // - Disable top-level `thinking` (remove `thinking` field). diff --git a/backend/internal/service/gateway_request_test.go b/backend/internal/service/gateway_request_test.go index 8bcc1ee1..f92496fb 100644 --- a/backend/internal/service/gateway_request_test.go +++ b/backend/internal/service/gateway_request_test.go @@ -176,11 +176,14 @@ func TestFilterThinkingBlocksForRetry_DisablesThinkingAndPreservesAsText(t *test require.True(t, ok) require.Len(t, msgs, 2) - assistant := msgs[1].(map[string]any) - content := assistant["content"].([]any) + assistant, ok := msgs[1].(map[string]any) + require.True(t, ok) + content, ok := assistant["content"].([]any) + require.True(t, ok) require.Len(t, content, 2) - first := content[0].(map[string]any) + first, ok := content[0].(map[string]any) + require.True(t, ok) require.Equal(t, "text", first["type"]) require.Equal(t, "Let me think...", first["text"]) } @@ -221,11 +224,17 @@ func TestFilterThinkingBlocksForRetry_RemovesRedactedThinkingAndKeepsValidConten _, hasThinking := req["thinking"] require.False(t, hasThinking) - msgs := req["messages"].([]any) - content := msgs[0].(map[string]any)["content"].([]any) + msgs, ok := req["messages"].([]any) + require.True(t, ok) + msg0, ok := msgs[0].(map[string]any) + require.True(t, ok) + content, ok := msg0["content"].([]any) + require.True(t, ok) require.Len(t, content, 1) - require.Equal(t, "text", content[0].(map[string]any)["type"]) - require.Equal(t, "Visible", content[0].(map[string]any)["text"]) + content0, ok := content[0].(map[string]any) + require.True(t, ok) + require.Equal(t, "text", content0["type"]) + require.Equal(t, "Visible", content0["text"]) } func TestFilterThinkingBlocksForRetry_EmptyContentGetsPlaceholder(t *testing.T) { @@ -240,11 +249,17 @@ func TestFilterThinkingBlocksForRetry_EmptyContentGetsPlaceholder(t *testing.T) var req map[string]any require.NoError(t, json.Unmarshal(out, &req)) - msgs := req["messages"].([]any) - content := msgs[0].(map[string]any)["content"].([]any) + msgs, ok := req["messages"].([]any) + require.True(t, ok) + msg0, ok := msgs[0].(map[string]any) + require.True(t, ok) + content, ok := msg0["content"].([]any) + require.True(t, ok) require.Len(t, content, 1) - require.Equal(t, "text", content[0].(map[string]any)["type"]) - require.NotEmpty(t, content[0].(map[string]any)["text"]) + content0, ok := content[0].(map[string]any) + require.True(t, ok) + require.Equal(t, "text", content0["type"]) + require.NotEmpty(t, content0["text"]) } func TestFilterSignatureSensitiveBlocksForRetry_DowngradesTools(t *testing.T) { @@ -265,11 +280,19 @@ func TestFilterSignatureSensitiveBlocksForRetry_DowngradesTools(t *testing.T) { _, hasThinking := req["thinking"] require.False(t, hasThinking) - msgs := req["messages"].([]any) - content := msgs[0].(map[string]any)["content"].([]any) + msgs, ok := req["messages"].([]any) + require.True(t, ok) + msg0, ok := msgs[0].(map[string]any) + require.True(t, ok) + content, ok := msg0["content"].([]any) + require.True(t, ok) require.Len(t, content, 2) - require.Equal(t, "text", content[0].(map[string]any)["type"]) - require.Equal(t, "text", content[1].(map[string]any)["type"]) - require.Contains(t, content[0].(map[string]any)["text"], "tool_use") - require.Contains(t, content[1].(map[string]any)["text"], "tool_result") + content0, ok := content[0].(map[string]any) + require.True(t, ok) + content1, ok := content[1].(map[string]any) + require.True(t, ok) + require.Equal(t, "text", content0["type"]) + require.Equal(t, "text", content1["type"]) + require.Contains(t, content0["text"], "tool_use") + require.Contains(t, content1["text"], "tool_result") } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 5d39c01d..dcde757c 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -1135,90 +1135,90 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A // 优先检测thinking block签名错误(400)并重试一次 if resp.StatusCode == 400 { respBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) - if readErr == nil { - _ = resp.Body.Close() + if readErr == nil { + _ = resp.Body.Close() - if s.isThinkingBlockSignatureError(respBody) { - looksLikeToolSignatureError := func(msg string) bool { - m := strings.ToLower(msg) - return strings.Contains(m, "tool_use") || - strings.Contains(m, "tool_result") || - strings.Contains(m, "functioncall") || - strings.Contains(m, "function_call") || - strings.Contains(m, "functionresponse") || - strings.Contains(m, "function_response") - } - - // 避免在重试预算已耗尽时再发起额外请求 - if time.Since(retryStart) >= maxRetryElapsed { - resp.Body = io.NopCloser(bytes.NewReader(respBody)) - break + if s.isThinkingBlockSignatureError(respBody) { + looksLikeToolSignatureError := func(msg string) bool { + m := strings.ToLower(msg) + return strings.Contains(m, "tool_use") || + strings.Contains(m, "tool_result") || + strings.Contains(m, "functioncall") || + strings.Contains(m, "function_call") || + strings.Contains(m, "functionresponse") || + strings.Contains(m, "function_response") } - log.Printf("Account %d: detected thinking block signature error, retrying with filtered thinking blocks", account.ID) - // Conservative two-stage fallback: - // 1) Disable thinking + thinking->text (preserve content) - // 2) Only if upstream still errors AND error message points to tool/function signature issues: - // also downgrade tool_use/tool_result blocks to text. - - filteredBody := FilterThinkingBlocksForRetry(body) - retryReq, buildErr := s.buildUpstreamRequest(ctx, c, account, filteredBody, token, tokenType, reqModel) - if buildErr == nil { - retryResp, retryErr := s.httpUpstream.Do(retryReq, proxyURL, account.ID, account.Concurrency) - if retryErr == nil { - if retryResp.StatusCode < 400 { - log.Printf("Account %d: signature error retry succeeded (thinking downgraded)", account.ID) - resp = retryResp - break - } - - retryRespBody, retryReadErr := io.ReadAll(io.LimitReader(retryResp.Body, 2<<20)) - _ = retryResp.Body.Close() - if retryReadErr == nil && retryResp.StatusCode == 400 && s.isThinkingBlockSignatureError(retryRespBody) { - msg2 := extractUpstreamErrorMessage(retryRespBody) - if looksLikeToolSignatureError(msg2) && time.Since(retryStart) < maxRetryElapsed { - log.Printf("Account %d: signature retry still failing and looks tool-related, retrying with tool blocks downgraded", account.ID) - filteredBody2 := FilterSignatureSensitiveBlocksForRetry(body) - retryReq2, buildErr2 := s.buildUpstreamRequest(ctx, c, account, filteredBody2, token, tokenType, reqModel) - if buildErr2 == nil { - retryResp2, retryErr2 := s.httpUpstream.Do(retryReq2, proxyURL, account.ID, account.Concurrency) - if retryErr2 == nil { - resp = retryResp2 - break - } - if retryResp2 != nil && retryResp2.Body != nil { - _ = retryResp2.Body.Close() - } - log.Printf("Account %d: tool-downgrade signature retry failed: %v", account.ID, retryErr2) - } else { - log.Printf("Account %d: tool-downgrade signature retry build failed: %v", account.ID, buildErr2) - } - } - } - - // Fall back to the original retry response context. - resp = &http.Response{ - StatusCode: retryResp.StatusCode, - Header: retryResp.Header.Clone(), - Body: io.NopCloser(bytes.NewReader(retryRespBody)), - } - break - } - if retryResp != nil && retryResp.Body != nil { - _ = retryResp.Body.Close() - } - log.Printf("Account %d: signature error retry failed: %v", account.ID, retryErr) - } else { - log.Printf("Account %d: signature error retry build request failed: %v", account.ID, buildErr) - } - - // Retry failed: restore original response body and continue handling. + // 避免在重试预算已耗尽时再发起额外请求 + if time.Since(retryStart) >= maxRetryElapsed { resp.Body = io.NopCloser(bytes.NewReader(respBody)) break } - // 不是thinking签名错误,恢复响应体 + log.Printf("Account %d: detected thinking block signature error, retrying with filtered thinking blocks", account.ID) + + // Conservative two-stage fallback: + // 1) Disable thinking + thinking->text (preserve content) + // 2) Only if upstream still errors AND error message points to tool/function signature issues: + // also downgrade tool_use/tool_result blocks to text. + + filteredBody := FilterThinkingBlocksForRetry(body) + retryReq, buildErr := s.buildUpstreamRequest(ctx, c, account, filteredBody, token, tokenType, reqModel) + if buildErr == nil { + retryResp, retryErr := s.httpUpstream.Do(retryReq, proxyURL, account.ID, account.Concurrency) + if retryErr == nil { + if retryResp.StatusCode < 400 { + log.Printf("Account %d: signature error retry succeeded (thinking downgraded)", account.ID) + resp = retryResp + break + } + + retryRespBody, retryReadErr := io.ReadAll(io.LimitReader(retryResp.Body, 2<<20)) + _ = retryResp.Body.Close() + if retryReadErr == nil && retryResp.StatusCode == 400 && s.isThinkingBlockSignatureError(retryRespBody) { + msg2 := extractUpstreamErrorMessage(retryRespBody) + if looksLikeToolSignatureError(msg2) && time.Since(retryStart) < maxRetryElapsed { + log.Printf("Account %d: signature retry still failing and looks tool-related, retrying with tool blocks downgraded", account.ID) + filteredBody2 := FilterSignatureSensitiveBlocksForRetry(body) + retryReq2, buildErr2 := s.buildUpstreamRequest(ctx, c, account, filteredBody2, token, tokenType, reqModel) + if buildErr2 == nil { + retryResp2, retryErr2 := s.httpUpstream.Do(retryReq2, proxyURL, account.ID, account.Concurrency) + if retryErr2 == nil { + resp = retryResp2 + break + } + if retryResp2 != nil && retryResp2.Body != nil { + _ = retryResp2.Body.Close() + } + log.Printf("Account %d: tool-downgrade signature retry failed: %v", account.ID, retryErr2) + } else { + log.Printf("Account %d: tool-downgrade signature retry build failed: %v", account.ID, buildErr2) + } + } + } + + // Fall back to the original retry response context. + resp = &http.Response{ + StatusCode: retryResp.StatusCode, + Header: retryResp.Header.Clone(), + Body: io.NopCloser(bytes.NewReader(retryRespBody)), + } + break + } + if retryResp != nil && retryResp.Body != nil { + _ = retryResp.Body.Close() + } + log.Printf("Account %d: signature error retry failed: %v", account.ID, retryErr) + } else { + log.Printf("Account %d: signature error retry build request failed: %v", account.ID, buildErr) + } + + // Retry failed: restore original response body and continue handling. resp.Body = io.NopCloser(bytes.NewReader(respBody)) + break } + // 不是thinking签名错误,恢复响应体 + resp.Body = io.NopCloser(bytes.NewReader(respBody)) + } } // 检查是否需要通用重试(排除400,因为400已经在上面特殊处理过了)