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_anthropic": "claude-3-5-sonnet-20241022",
|
||||||
"fallback_model_antigravity": "gemini-2.5-pro",
|
"fallback_model_antigravity": "gemini-2.5-pro",
|
||||||
"fallback_model_gemini": "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{
|
Messages: []antigravity.ClaudeMessage{
|
||||||
{
|
{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: json.RawMessage(`[
|
Content: json.RawMessage(`[
|
||||||
{"type":"thinking","thinking":"secret plan","signature":""},
|
{"type":"thinking","thinking":"secret plan","signature":""},
|
||||||
{"type":"tool_use","id":"t1","name":"Bash","input":{"command":"ls"}}
|
{"type":"tool_use","id":"t1","name":"Bash","input":{"command":"ls"}}
|
||||||
]`),
|
]`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: json.RawMessage(`[
|
Content: json.RawMessage(`[
|
||||||
{"type":"tool_result","tool_use_id":"t1","content":"ok","is_error":false},
|
{"type":"tool_result","tool_use_id":"t1","content":"ok","is_error":false},
|
||||||
{"type":"redacted_thinking","data":"..."}
|
{"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
|
// - 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.
|
// 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:
|
// - 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):
|
// Strategy (B: preserve content as text):
|
||||||
// - Disable top-level `thinking` (remove `thinking` field).
|
// - Disable top-level `thinking` (remove `thinking` field).
|
||||||
|
|||||||
@@ -176,11 +176,14 @@ func TestFilterThinkingBlocksForRetry_DisablesThinkingAndPreservesAsText(t *test
|
|||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
require.Len(t, msgs, 2)
|
require.Len(t, msgs, 2)
|
||||||
|
|
||||||
assistant := msgs[1].(map[string]any)
|
assistant, ok := msgs[1].(map[string]any)
|
||||||
content := assistant["content"].([]any)
|
require.True(t, ok)
|
||||||
|
content, ok := assistant["content"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
require.Len(t, content, 2)
|
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, "text", first["type"])
|
||||||
require.Equal(t, "Let me think...", first["text"])
|
require.Equal(t, "Let me think...", first["text"])
|
||||||
}
|
}
|
||||||
@@ -221,11 +224,17 @@ func TestFilterThinkingBlocksForRetry_RemovesRedactedThinkingAndKeepsValidConten
|
|||||||
_, hasThinking := req["thinking"]
|
_, hasThinking := req["thinking"]
|
||||||
require.False(t, hasThinking)
|
require.False(t, hasThinking)
|
||||||
|
|
||||||
msgs := req["messages"].([]any)
|
msgs, ok := req["messages"].([]any)
|
||||||
content := msgs[0].(map[string]any)["content"].([]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.Len(t, content, 1)
|
||||||
require.Equal(t, "text", content[0].(map[string]any)["type"])
|
content0, ok := content[0].(map[string]any)
|
||||||
require.Equal(t, "Visible", content[0].(map[string]any)["text"])
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "text", content0["type"])
|
||||||
|
require.Equal(t, "Visible", content0["text"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterThinkingBlocksForRetry_EmptyContentGetsPlaceholder(t *testing.T) {
|
func TestFilterThinkingBlocksForRetry_EmptyContentGetsPlaceholder(t *testing.T) {
|
||||||
@@ -240,11 +249,17 @@ func TestFilterThinkingBlocksForRetry_EmptyContentGetsPlaceholder(t *testing.T)
|
|||||||
|
|
||||||
var req map[string]any
|
var req map[string]any
|
||||||
require.NoError(t, json.Unmarshal(out, &req))
|
require.NoError(t, json.Unmarshal(out, &req))
|
||||||
msgs := req["messages"].([]any)
|
msgs, ok := req["messages"].([]any)
|
||||||
content := msgs[0].(map[string]any)["content"].([]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.Len(t, content, 1)
|
||||||
require.Equal(t, "text", content[0].(map[string]any)["type"])
|
content0, ok := content[0].(map[string]any)
|
||||||
require.NotEmpty(t, content[0].(map[string]any)["text"])
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "text", content0["type"])
|
||||||
|
require.NotEmpty(t, content0["text"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterSignatureSensitiveBlocksForRetry_DowngradesTools(t *testing.T) {
|
func TestFilterSignatureSensitiveBlocksForRetry_DowngradesTools(t *testing.T) {
|
||||||
@@ -265,11 +280,19 @@ func TestFilterSignatureSensitiveBlocksForRetry_DowngradesTools(t *testing.T) {
|
|||||||
_, hasThinking := req["thinking"]
|
_, hasThinking := req["thinking"]
|
||||||
require.False(t, hasThinking)
|
require.False(t, hasThinking)
|
||||||
|
|
||||||
msgs := req["messages"].([]any)
|
msgs, ok := req["messages"].([]any)
|
||||||
content := msgs[0].(map[string]any)["content"].([]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.Len(t, content, 2)
|
||||||
require.Equal(t, "text", content[0].(map[string]any)["type"])
|
content0, ok := content[0].(map[string]any)
|
||||||
require.Equal(t, "text", content[1].(map[string]any)["type"])
|
require.True(t, ok)
|
||||||
require.Contains(t, content[0].(map[string]any)["text"], "tool_use")
|
content1, ok := content[1].(map[string]any)
|
||||||
require.Contains(t, content[1].(map[string]any)["text"], "tool_result")
|
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)并重试一次
|
// 优先检测thinking block签名错误(400)并重试一次
|
||||||
if resp.StatusCode == 400 {
|
if resp.StatusCode == 400 {
|
||||||
respBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
|
respBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
|
||||||
if readErr == nil {
|
if readErr == nil {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
if s.isThinkingBlockSignatureError(respBody) {
|
if s.isThinkingBlockSignatureError(respBody) {
|
||||||
looksLikeToolSignatureError := func(msg string) bool {
|
looksLikeToolSignatureError := func(msg string) bool {
|
||||||
m := strings.ToLower(msg)
|
m := strings.ToLower(msg)
|
||||||
return strings.Contains(m, "tool_use") ||
|
return strings.Contains(m, "tool_use") ||
|
||||||
strings.Contains(m, "tool_result") ||
|
strings.Contains(m, "tool_result") ||
|
||||||
strings.Contains(m, "functioncall") ||
|
strings.Contains(m, "functioncall") ||
|
||||||
strings.Contains(m, "function_call") ||
|
strings.Contains(m, "function_call") ||
|
||||||
strings.Contains(m, "functionresponse") ||
|
strings.Contains(m, "functionresponse") ||
|
||||||
strings.Contains(m, "function_response")
|
strings.Contains(m, "function_response")
|
||||||
}
|
|
||||||
|
|
||||||
// 避免在重试预算已耗尽时再发起额外请求
|
|
||||||
if time.Since(retryStart) >= maxRetryElapsed {
|
|
||||||
resp.Body = io.NopCloser(bytes.NewReader(respBody))
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
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)
|
if time.Since(retryStart) >= maxRetryElapsed {
|
||||||
// 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))
|
resp.Body = io.NopCloser(bytes.NewReader(respBody))
|
||||||
break
|
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))
|
resp.Body = io.NopCloser(bytes.NewReader(respBody))
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
// 不是thinking签名错误,恢复响应体
|
||||||
|
resp.Body = io.NopCloser(bytes.NewReader(respBody))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要通用重试(排除400,因为400已经在上面特殊处理过了)
|
// 检查是否需要通用重试(排除400,因为400已经在上面特殊处理过了)
|
||||||
|
|||||||
Reference in New Issue
Block a user