fix(backend): 修复 CI 失败问题
修复内容: 1. 修复 6 个 golangci-lint 错误 - 3 个 errcheck 错误:在 gateway_request_test.go 中添加类型断言检查 - 3 个 gofmt 格式化问题:修复代码格式 2. 修复 API 契约测试失败 - 在测试中添加缺失的字段:enable_identity_patch 和 identity_patch_prompt 所有测试和 linter 检查现已通过。
This commit is contained in:
@@ -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": ""
|
||||
}
|
||||
}`,
|
||||
},
|
||||
|
||||
@@ -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":"..."}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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已经在上面特殊处理过了)
|
||||
|
||||
Reference in New Issue
Block a user