From 19cca11e00c41c1dfd20f9a90589d7d55b61b4e1 Mon Sep 17 00:00:00 2001 From: SilentFlower Date: Wed, 11 Feb 2026 10:31:16 +0800 Subject: [PATCH 1/2] =?UTF-8?q?[UPDATE]=20=E5=A2=9E=E5=BC=BA=20Claude=20Th?= =?UTF-8?q?inking=20=E6=A8=A1=E5=BC=8F=E6=94=AF=E6=8C=81=E4=B8=8E=20Opus?= =?UTF-8?q?=204.6=20=E5=8A=A8=E6=80=81=E9=A2=84=E7=AE=97=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ feat(antigravity): 支持 thinking adaptive 类型并适配 Opus 4.6 动态预算 🧪 test(gateway): 增加 thinking 模式解析与签名块过滤的边界用例测试 --- .../internal/pkg/antigravity/claude_types.go | 4 +- .../pkg/antigravity/request_transformer.go | 33 +++++-- .../antigravity/request_transformer_test.go | 90 +++++++++++++++++++ .../service/antigravity_gateway_service.go | 2 +- backend/internal/service/gateway_request.go | 18 ++-- .../internal/service/gateway_request_test.go | 18 ++++ backend/internal/service/gateway_service.go | 3 +- 7 files changed, 148 insertions(+), 20 deletions(-) diff --git a/backend/internal/pkg/antigravity/claude_types.go b/backend/internal/pkg/antigravity/claude_types.go index 8a29cd10..8cd42298 100644 --- a/backend/internal/pkg/antigravity/claude_types.go +++ b/backend/internal/pkg/antigravity/claude_types.go @@ -27,8 +27,8 @@ type ClaudeMessage struct { // ThinkingConfig Thinking 配置 type ThinkingConfig struct { - Type string `json:"type"` // "enabled" or "disabled" - BudgetTokens int `json:"budget_tokens,omitempty"` // thinking budget + Type string `json:"type"` // "enabled" / "adaptive" / "disabled" + BudgetTokens int `json:"budget_tokens,omitempty"` // thinking budget(-1 表示动态预算) } // ClaudeMetadata 请求元数据 diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index e89a4c53..e20b00db 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -64,6 +64,10 @@ const MaxTokensBudgetPadding = 1000 // Gemini 2.5 Flash thinking budget 上限 const Gemini25FlashThinkingBudgetLimit = 24576 +// 对于 Antigravity 的 Claude(budget-only)模型,该语义最终等价为 thinkingBudget=24576。 +// 这里复用相同数值以保持行为一致。 +const ClaudeAdaptiveHighThinkingBudgetTokens = Gemini25FlashThinkingBudgetLimit + // ensureMaxTokensGreaterThanBudget 确保 max_tokens > budget_tokens // Claude API 要求启用 thinking 时,max_tokens 必须大于 thinking.budget_tokens // 返回调整后的 maxTokens 和是否进行了调整 @@ -96,7 +100,7 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map } // 检测是否启用 thinking - isThinkingEnabled := claudeReq.Thinking != nil && claudeReq.Thinking.Type == "enabled" + isThinkingEnabled := claudeReq.Thinking != nil && (claudeReq.Thinking.Type == "enabled" || claudeReq.Thinking.Type == "adaptive") // 只有 Gemini 模型支持 dummy thought workaround // Claude 模型通过 Vertex/Google API 需要有效的 thought signatures @@ -198,8 +202,7 @@ type modelInfo struct { // modelInfoMap 模型前缀 → 模型信息映射 // 只有在此映射表中的模型才会注入身份提示词 -// 注意:当前 claude-opus-4-6 会被映射到 claude-opus-4-5-thinking, -// 但保留此条目以便后续 Antigravity 上游支持 4.6 时快速切换 +// 注意:模型映射逻辑在网关层完成;这里仅用于按模型前缀判断是否注入身份提示词。 var modelInfoMap = map[string]modelInfo{ "claude-opus-4-5": {DisplayName: "Claude Opus 4.5", CanonicalID: "claude-opus-4-5-20250929"}, "claude-opus-4-6": {DisplayName: "Claude Opus 4.6", CanonicalID: "claude-opus-4-6"}, @@ -593,6 +596,10 @@ func maxOutputTokensLimit(model string) int { return maxOutputTokensUpperBound } +func isAntigravityOpus46Model(model string) bool { + return strings.HasPrefix(strings.ToLower(model), "claude-opus-4-6") +} + func buildGenerationConfig(req *ClaudeRequest) *GeminiGenerationConfig { maxLimit := maxOutputTokensLimit(req.Model) config := &GeminiGenerationConfig{ @@ -606,25 +613,37 @@ func buildGenerationConfig(req *ClaudeRequest) *GeminiGenerationConfig { } // Thinking 配置 - if req.Thinking != nil && req.Thinking.Type == "enabled" { + if req.Thinking != nil && (req.Thinking.Type == "enabled" || req.Thinking.Type == "adaptive") { config.ThinkingConfig = &GeminiThinkingConfig{ IncludeThoughts: true, } + + // - thinking.type=enabled:budget_tokens>0 用显式预算;否则使用动态预算(-1) + // - thinking.type=adaptive:仅在 Antigravity 的 Opus 4.6 上覆写为 (24576) + // (CLIProxyAPI 先翻译为 thinkingLevel="high",再按模型能力转换为预算值)。 + budget := -1 if req.Thinking.BudgetTokens > 0 { - budget := req.Thinking.BudgetTokens + budget = req.Thinking.BudgetTokens + } + if req.Thinking.Type == "adaptive" && isAntigravityOpus46Model(req.Model) { + budget = ClaudeAdaptiveHighThinkingBudgetTokens + } + + // 正预算需要做上限与 max_tokens 约束;动态预算(-1)直接透传给上游。 + if budget > 0 { // gemini-2.5-flash 上限 if strings.Contains(req.Model, "gemini-2.5-flash") && budget > Gemini25FlashThinkingBudgetLimit { budget = Gemini25FlashThinkingBudgetLimit } - config.ThinkingConfig.ThinkingBudget = budget - // 自动修正:max_tokens 必须大于 budget_tokens + // 自动修正:max_tokens 必须大于 budget_tokens(Claude 上游要求) if adjusted, ok := ensureMaxTokensGreaterThanBudget(config.MaxOutputTokens, budget); ok { log.Printf("[Antigravity] Auto-adjusted max_tokens from %d to %d (must be > budget_tokens=%d)", config.MaxOutputTokens, adjusted, budget) config.MaxOutputTokens = adjusted } } + config.ThinkingConfig.ThinkingBudget = budget } if config.MaxOutputTokens > maxLimit { diff --git a/backend/internal/pkg/antigravity/request_transformer_test.go b/backend/internal/pkg/antigravity/request_transformer_test.go index f938b47f..f267e0e1 100644 --- a/backend/internal/pkg/antigravity/request_transformer_test.go +++ b/backend/internal/pkg/antigravity/request_transformer_test.go @@ -259,3 +259,93 @@ func TestBuildTools_CustomTypeTools(t *testing.T) { }) } } + +func TestBuildGenerationConfig_ThinkingDynamicBudget(t *testing.T) { + tests := []struct { + name string + model string + thinking *ThinkingConfig + wantBudget int + wantPresent bool + }{ + { + name: "enabled without budget defaults to dynamic (-1)", + model: "claude-opus-4-6-thinking", + thinking: &ThinkingConfig{Type: "enabled"}, + wantBudget: -1, + wantPresent: true, + }, + { + name: "enabled with budget uses the provided value", + model: "claude-opus-4-6-thinking", + thinking: &ThinkingConfig{Type: "enabled", BudgetTokens: 1024}, + wantBudget: 1024, + wantPresent: true, + }, + { + name: "enabled with -1 budget uses dynamic (-1)", + model: "claude-opus-4-6-thinking", + thinking: &ThinkingConfig{Type: "enabled", BudgetTokens: -1}, + wantBudget: -1, + wantPresent: true, + }, + { + name: "adaptive on opus4.6 maps to high budget (24576)", + model: "claude-opus-4-6-thinking", + thinking: &ThinkingConfig{Type: "adaptive", BudgetTokens: 20000}, + wantBudget: ClaudeAdaptiveHighThinkingBudgetTokens, + wantPresent: true, + }, + { + name: "adaptive on non-opus model keeps default dynamic (-1)", + model: "claude-sonnet-4-5-thinking", + thinking: &ThinkingConfig{Type: "adaptive"}, + wantBudget: -1, + wantPresent: true, + }, + { + name: "disabled does not emit thinkingConfig", + model: "claude-opus-4-6-thinking", + thinking: &ThinkingConfig{Type: "disabled", BudgetTokens: 1024}, + wantBudget: 0, + wantPresent: false, + }, + { + name: "nil thinking does not emit thinkingConfig", + model: "claude-opus-4-6-thinking", + thinking: nil, + wantBudget: 0, + wantPresent: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := &ClaudeRequest{ + Model: tt.model, + Thinking: tt.thinking, + } + cfg := buildGenerationConfig(req) + if cfg == nil { + t.Fatalf("expected non-nil generationConfig") + } + + if tt.wantPresent { + if cfg.ThinkingConfig == nil { + t.Fatalf("expected thinkingConfig to be present") + } + if !cfg.ThinkingConfig.IncludeThoughts { + t.Fatalf("expected includeThoughts=true") + } + if cfg.ThinkingConfig.ThinkingBudget != tt.wantBudget { + t.Fatalf("expected thinkingBudget=%d, got %d", tt.wantBudget, cfg.ThinkingConfig.ThinkingBudget) + } + return + } + + if cfg.ThinkingConfig != nil { + t.Fatalf("expected thinkingConfig to be nil, got %+v", cfg.ThinkingConfig) + } + }) + } +} diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 0d054c49..9e7ed7d5 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -1309,7 +1309,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, return nil, s.writeClaudeError(c, http.StatusForbidden, "permission_error", fmt.Sprintf("model %s not in whitelist", claudeReq.Model)) } // 应用 thinking 模式自动后缀:如果 thinking 开启且目标是 claude-sonnet-4-5,自动改为 thinking 版本 - thinkingEnabled := claudeReq.Thinking != nil && claudeReq.Thinking.Type == "enabled" + thinkingEnabled := claudeReq.Thinking != nil && (claudeReq.Thinking.Type == "enabled" || claudeReq.Thinking.Type == "adaptive") mappedModel = applyThinkingModelSuffix(mappedModel, thinkingEnabled) // 获取 access_token diff --git a/backend/internal/service/gateway_request.go b/backend/internal/service/gateway_request.go index c039f030..743dd738 100644 --- a/backend/internal/service/gateway_request.go +++ b/backend/internal/service/gateway_request.go @@ -101,9 +101,9 @@ func ParseGatewayRequest(body []byte, protocol string) (*ParsedRequest, error) { } } - // thinking: {type: "enabled"} + // thinking: {type: "enabled" | "adaptive"} if rawThinking, ok := req["thinking"].(map[string]any); ok { - if t, ok := rawThinking["type"].(string); ok && t == "enabled" { + if t, ok := rawThinking["type"].(string); ok && (t == "enabled" || t == "adaptive") { parsed.ThinkingEnabled = true } } @@ -161,9 +161,9 @@ func parseIntegralNumber(raw any) (int, bool) { // Returns filtered body or original body if filtering fails (fail-safe) // This prevents 400 errors from invalid thinking block signatures // -// Strategy: -// - When thinking.type != "enabled": Remove all thinking blocks -// - When thinking.type == "enabled": Only remove thinking blocks without valid signatures +// 策略: +// - 当 thinking.type 不是 "enabled"/"adaptive":移除所有 thinking 相关块 +// - 当 thinking.type 是 "enabled"/"adaptive":仅移除缺失/无效 signature 的 thinking 块(避免 400) // (blocks with missing/empty/dummy signatures that would cause 400 errors) func FilterThinkingBlocks(body []byte) []byte { return filterThinkingBlocksInternal(body, false) @@ -489,9 +489,9 @@ func FilterSignatureSensitiveBlocksForRetry(body []byte) []byte { } // filterThinkingBlocksInternal removes invalid thinking blocks from request -// Strategy: -// - When thinking.type != "enabled": Remove all thinking blocks -// - When thinking.type == "enabled": Only remove thinking blocks without valid signatures +// 策略: +// - 当 thinking.type 不是 "enabled"/"adaptive":移除所有 thinking 相关块 +// - 当 thinking.type 是 "enabled"/"adaptive":仅移除缺失/无效 signature 的 thinking 块 func filterThinkingBlocksInternal(body []byte, _ bool) []byte { // Fast path: if body doesn't contain "thinking", skip parsing if !bytes.Contains(body, []byte(`"type":"thinking"`)) && @@ -511,7 +511,7 @@ func filterThinkingBlocksInternal(body []byte, _ bool) []byte { // Check if thinking is enabled thinkingEnabled := false if thinking, ok := req["thinking"].(map[string]any); ok { - if thinkType, ok := thinking["type"].(string); ok && thinkType == "enabled" { + if thinkType, ok := thinking["type"].(string); ok && (thinkType == "enabled" || thinkType == "adaptive") { thinkingEnabled = true } } diff --git a/backend/internal/service/gateway_request_test.go b/backend/internal/service/gateway_request_test.go index cef41c91..5b85e752 100644 --- a/backend/internal/service/gateway_request_test.go +++ b/backend/internal/service/gateway_request_test.go @@ -29,6 +29,14 @@ func TestParseGatewayRequest_ThinkingEnabled(t *testing.T) { require.True(t, parsed.ThinkingEnabled) } +func TestParseGatewayRequest_ThinkingAdaptiveEnabled(t *testing.T) { + body := []byte(`{"model":"claude-sonnet-4-5","thinking":{"type":"adaptive"},"messages":[{"content":"hi"}]}`) + parsed, err := ParseGatewayRequest(body, "") + require.NoError(t, err) + require.Equal(t, "claude-sonnet-4-5", parsed.Model) + require.True(t, parsed.ThinkingEnabled) +} + func TestParseGatewayRequest_MaxTokens(t *testing.T) { body := []byte(`{"model":"claude-haiku-4-5","max_tokens":1}`) parsed, err := ParseGatewayRequest(body, "") @@ -209,6 +217,16 @@ func TestFilterThinkingBlocks(t *testing.T) { input: `{"model":"claude-3-5-sonnet-20241022","messages":[{"role":"user","content":[{"type":"text","text":"Hello"},{"type":"thinking","thinking":"internal","signature":"invalid"},{"type":"text","text":"World"}]}]}`, shouldFilter: true, }, + { + name: "does not filter signed thinking blocks when thinking adaptive", + input: `{"thinking":{"type":"adaptive"},"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"ok","signature":"sig_real_123"},{"type":"text","text":"B"}]}]}`, + shouldFilter: false, + }, + { + name: "filters unsigned thinking blocks when thinking adaptive", + input: `{"thinking":{"type":"adaptive"},"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"internal","signature":""},{"type":"text","text":"B"}]}]}`, + shouldFilter: true, + }, { name: "handles no thinking blocks", input: `{"model":"claude-3-5-sonnet-20241022","messages":[{"role":"user","content":[{"type":"text","text":"Hello"}]}]}`, diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 71b1f594..56af4610 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -3631,7 +3631,8 @@ func requestNeedsBetaFeatures(body []byte) bool { if tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 { return true } - if strings.EqualFold(gjson.GetBytes(body, "thinking.type").String(), "enabled") { + thinkingType := gjson.GetBytes(body, "thinking.type").String() + if strings.EqualFold(thinkingType, "enabled") || strings.EqualFold(thinkingType, "adaptive") { return true } return false From 6ae82e04d5cf8aaed60f5e3c8431fd1d803a66e8 Mon Sep 17 00:00:00 2001 From: SilentFlower Date: Wed, 11 Feb 2026 10:39:54 +0800 Subject: [PATCH 2/2] =?UTF-8?q?[UPDATE]=20=E4=BC=98=E5=8C=96=E6=80=9D?= =?UTF-8?q?=E8=80=83=E9=A2=84=E7=AE=97=E9=80=BB=E8=BE=91=E4=B8=8E=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🧠 refactor(antigravity): 完善 thinking 预算分配策略并重构工具构建逻辑 --- backend/internal/pkg/antigravity/claude_types.go | 2 +- backend/internal/pkg/antigravity/request_transformer.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/internal/pkg/antigravity/claude_types.go b/backend/internal/pkg/antigravity/claude_types.go index 8cd42298..7c127b90 100644 --- a/backend/internal/pkg/antigravity/claude_types.go +++ b/backend/internal/pkg/antigravity/claude_types.go @@ -28,7 +28,7 @@ type ClaudeMessage struct { // ThinkingConfig Thinking 配置 type ThinkingConfig struct { Type string `json:"type"` // "enabled" / "adaptive" / "disabled" - BudgetTokens int `json:"budget_tokens,omitempty"` // thinking budget(-1 表示动态预算) + BudgetTokens int `json:"budget_tokens,omitempty"` // thinking budget } // ClaudeMetadata 请求元数据 diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index e20b00db..3ba04b95 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -618,9 +618,8 @@ func buildGenerationConfig(req *ClaudeRequest) *GeminiGenerationConfig { IncludeThoughts: true, } - // - thinking.type=enabled:budget_tokens>0 用显式预算;否则使用动态预算(-1) + // - thinking.type=enabled:budget_tokens>0 用显式预算 // - thinking.type=adaptive:仅在 Antigravity 的 Opus 4.6 上覆写为 (24576) - // (CLIProxyAPI 先翻译为 thinkingLevel="high",再按模型能力转换为预算值)。 budget := -1 if req.Thinking.BudgetTokens > 0 { budget = req.Thinking.BudgetTokens