fix: 收敛 Claude Code 探测拦截并补齐回归测试

This commit is contained in:
shaw
2026-02-07 19:04:08 +08:00
parent da9546ba24
commit 6aaa4aee6a
7 changed files with 290 additions and 26 deletions

View File

@@ -2,6 +2,7 @@ package handler
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
@@ -111,9 +112,6 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
return
}
// 检查是否为 Claude Code 客户端,设置到 context 中
SetClaudeCodeClientContext(c, body)
setOpsRequestContext(c, "", false, body)
parsedReq, err := service.ParseGatewayRequest(body)
@@ -121,11 +119,23 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
return
}
// 在请求上下文中记录 thinking 状态,供 Antigravity 最终模型 key 推导/模型维度限流使用
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), ctxkey.ThinkingEnabled, parsedReq.ThinkingEnabled))
reqModel := parsedReq.Model
reqStream := parsedReq.Stream
// 设置 max_tokens=1 + haiku 探测请求标识到 context 中
// 必须在 SetClaudeCodeClientContext 之前设置,因为 ClaudeCodeValidator 需要读取此标识进行绕过判断
if isMaxTokensOneHaikuRequest(reqModel, parsedReq.MaxTokens, reqStream) {
ctx := context.WithValue(c.Request.Context(), ctxkey.IsMaxTokensOneHaikuRequest, true)
c.Request = c.Request.WithContext(ctx)
}
// 检查是否为 Claude Code 客户端,设置到 context 中
SetClaudeCodeClientContext(c, body)
isClaudeCodeClient := service.IsClaudeCodeClient(c.Request.Context())
// 在请求上下文中记录 thinking 状态,供 Antigravity 最终模型 key 推导/模型维度限流使用
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), ctxkey.ThinkingEnabled, parsedReq.ThinkingEnabled))
setOpsRequestContext(c, reqModel, reqStream, body)
// 验证 model 必填
@@ -241,7 +251,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
// 检查请求拦截预热请求、SUGGESTION MODE等
if account.IsInterceptWarmupEnabled() {
interceptType := detectInterceptType(body)
interceptType := detectInterceptType(body, reqModel, parsedReq.MaxTokens, reqStream, isClaudeCodeClient)
if interceptType != InterceptTypeNone {
if selection.Acquired && selection.ReleaseFunc != nil {
selection.ReleaseFunc()
@@ -403,7 +413,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
// 检查请求拦截预热请求、SUGGESTION MODE等
if account.IsInterceptWarmupEnabled() {
interceptType := detectInterceptType(body)
interceptType := detectInterceptType(body, reqModel, parsedReq.MaxTokens, reqStream, isClaudeCodeClient)
if interceptType != InterceptTypeNone {
if selection.Acquired && selection.ReleaseFunc != nil {
selection.ReleaseFunc()
@@ -974,13 +984,37 @@ func (h *GatewayHandler) CountTokens(c *gin.Context) {
type InterceptType int
const (
InterceptTypeNone InterceptType = iota
InterceptTypeWarmup // 预热请求(返回 "New Conversation"
InterceptTypeSuggestionMode // SUGGESTION MODE返回空字符串
InterceptTypeNone InterceptType = iota
InterceptTypeWarmup // 预热请求(返回 "New Conversation"
InterceptTypeSuggestionMode // SUGGESTION MODE返回空字符串
InterceptTypeMaxTokensOneHaiku // max_tokens=1 + haiku 探测请求(返回 "#"
)
// isHaikuModel 检查模型名称是否包含 "haiku"(大小写不敏感)
func isHaikuModel(model string) bool {
return strings.Contains(strings.ToLower(model), "haiku")
}
// isMaxTokensOneHaikuRequest 检查是否为 max_tokens=1 + haiku 模型的探测请求
// 这类请求用于 Claude Code 验证 API 连通性
// 条件max_tokens == 1 且 model 包含 "haiku" 且非流式请求
func isMaxTokensOneHaikuRequest(model string, maxTokens int, isStream bool) bool {
return maxTokens == 1 && isHaikuModel(model) && !isStream
}
// detectInterceptType 检测请求是否需要拦截,返回拦截类型
func detectInterceptType(body []byte) InterceptType {
// 参数说明:
// - body: 请求体字节
// - model: 请求的模型名称
// - maxTokens: max_tokens 值
// - isStream: 是否为流式请求
// - isClaudeCodeClient: 是否已通过 Claude Code 客户端校验
func detectInterceptType(body []byte, model string, maxTokens int, isStream bool, isClaudeCodeClient bool) InterceptType {
// 优先检查 max_tokens=1 + haiku 探测请求(仅非流式)
if isClaudeCodeClient && isMaxTokensOneHaikuRequest(model, maxTokens, isStream) {
return InterceptTypeMaxTokensOneHaiku
}
// 快速检查:如果不包含任何关键字,直接返回
bodyStr := string(body)
hasSuggestionMode := strings.Contains(bodyStr, "[SUGGESTION MODE:")
@@ -1130,9 +1164,25 @@ func sendMockInterceptStream(c *gin.Context, model string, interceptType Interce
}
}
// generateRealisticMsgID 生成仿真的消息 IDmsg_bdrk_XXXXXXX 格式)
// 格式与 Claude API 真实响应一致24 位随机字母数字
func generateRealisticMsgID() string {
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const idLen = 24
randomBytes := make([]byte, idLen)
if _, err := rand.Read(randomBytes); err != nil {
return fmt.Sprintf("msg_bdrk_%d", time.Now().UnixNano())
}
b := make([]byte, idLen)
for i := range b {
b[i] = charset[int(randomBytes[i])%len(charset)]
}
return "msg_bdrk_" + string(b)
}
// sendMockInterceptResponse 发送非流式 mock 响应(用于请求拦截)
func sendMockInterceptResponse(c *gin.Context, model string, interceptType InterceptType) {
var msgID, text string
var msgID, text, stopReason string
var outputTokens int
switch interceptType {
@@ -1140,24 +1190,42 @@ func sendMockInterceptResponse(c *gin.Context, model string, interceptType Inter
msgID = "msg_mock_suggestion"
text = ""
outputTokens = 1
stopReason = "end_turn"
case InterceptTypeMaxTokensOneHaiku:
msgID = generateRealisticMsgID()
text = "#"
outputTokens = 1
stopReason = "max_tokens" // max_tokens=1 探测请求的 stop_reason 应为 max_tokens
default: // InterceptTypeWarmup
msgID = "msg_mock_warmup"
text = "New Conversation"
outputTokens = 2
stopReason = "end_turn"
}
c.JSON(http.StatusOK, gin.H{
"id": msgID,
"type": "message",
"role": "assistant",
"model": model,
"content": []gin.H{{"type": "text", "text": text}},
"stop_reason": "end_turn",
// 构建完整的响应格式(与 Claude API 响应格式一致)
response := gin.H{
"model": model,
"id": msgID,
"type": "message",
"role": "assistant",
"content": []gin.H{{"type": "text", "text": text}},
"stop_reason": stopReason,
"stop_sequence": nil,
"usage": gin.H{
"input_tokens": 10,
"input_tokens": 10,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"cache_creation": gin.H{
"ephemeral_5m_input_tokens": 0,
"ephemeral_1h_input_tokens": 0,
},
"output_tokens": outputTokens,
"total_tokens": 10 + outputTokens,
},
})
}
c.JSON(http.StatusOK, response)
}
func billingErrorDetails(err error) (status int, code, message string) {