diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index 4eaef1f6..fdc0f93e 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -14,6 +14,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -124,7 +125,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ signature = signatureResult.String() } - // P3: Try to restore signature from cache for unsigned thinking blocks + // Try to restore signature from cache for unsigned thinking blocks if !cache.HasValidSignature(signature) && sessionID != "" && thinkingText != "" { if cachedSig := cache.GetCachedSignature(sessionID, thinkingText); cachedSig != "" { signature = cachedSig @@ -132,7 +133,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ } } - // P2-A: Skip trailing unsigned thinking blocks on last assistant message + // Skip trailing unsigned thinking blocks on last assistant message isLastMessage := (i == numMessages-1) isLastContent := (j == numContents-1) isAssistant := (originalRole == "assistant") @@ -278,7 +279,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ out := `{"model":"","request":{"contents":[]}}` out, _ = sjson.Set(out, "model", modelName) - // P2-B: Inject interleaved thinking hint when both tools and thinking are active + // Inject interleaved thinking hint when both tools and thinking are active hasTools := toolDeclCount > 0 thinkingResult := gjson.GetBytes(rawJSON, "thinking") hasThinking := thinkingResult.Exists() && thinkingResult.IsObject() && thinkingResult.Get("type").String() == "enabled" diff --git a/internal/translator/antigravity/claude/antigravity_claude_request_test.go b/internal/translator/antigravity/claude/antigravity_claude_request_test.go index a5bfe49b..796ce0d3 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request_test.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request_test.go @@ -330,7 +330,7 @@ func TestConvertClaudeRequestToAntigravity_GenerationConfig(t *testing.T) { } // ============================================================================ -// P2-A: Trailing Unsigned Thinking Block Removal +// Trailing Unsigned Thinking Block Removal // ============================================================================ func TestConvertClaudeRequestToAntigravity_TrailingUnsignedThinking_Removed(t *testing.T) { @@ -435,7 +435,7 @@ func TestConvertClaudeRequestToAntigravity_MiddleUnsignedThinking_SentinelApplie } // ============================================================================ -// P2-B: Tool + Thinking System Hint Injection +// Tool + Thinking System Hint Injection // ============================================================================ func TestConvertClaudeRequestToAntigravity_ToolAndThinking_HintInjected(t *testing.T) { diff --git a/internal/translator/antigravity/claude/antigravity_claude_response.go b/internal/translator/antigravity/claude/antigravity_claude_response.go index d26a1c9f..939551ba 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_response.go +++ b/internal/translator/antigravity/claude/antigravity_claude_response.go @@ -41,7 +41,7 @@ type Params struct { HasToolUse bool // Indicates if tool use was observed in the stream HasContent bool // Tracks whether any content (text, thinking, or tool use) has been output - // P3: Signature caching support + // Signature caching support SessionID string // Session ID derived from request for signature caching CurrentThinkingText strings.Builder // Accumulates thinking text for signature caching } @@ -192,7 +192,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq output = output + fmt.Sprintf("data: %s\n\n\n", data) params.ResponseType = 2 // Set state to thinking params.HasContent = true - // P3: Start accumulating thinking text for signature caching + // Start accumulating thinking text for signature caching params.CurrentThinkingText.Reset() params.CurrentThinkingText.WriteString(partTextResult.String()) } @@ -276,8 +276,13 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq output = output + fmt.Sprintf("data: %s\n\n\n", data) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { + argsRaw := fcArgsResult.Raw + // Convert command → cmd for Bash tools using proper JSON parsing + if fcName == "Bash" || fcName == "bash" || fcName == "bash_20241022" { + argsRaw = convertBashCommandToCmdField(argsRaw) + } output = output + "event: content_block_delta\n" - data, _ = sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, params.ResponseIndex), "delta.partial_json", fcArgsResult.Raw) + data, _ = sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, params.ResponseIndex), "delta.partial_json", argsRaw) output = output + fmt.Sprintf("data: %s\n\n\n", data) } params.ResponseType = 3 @@ -365,6 +370,36 @@ func resolveStopReason(params *Params) string { return "end_turn" } +// convertBashCommandToCmdField converts "command" field to "cmd" field for Bash tools. +// Amp expects "cmd" but Gemini sends "command". This uses proper JSON parsing +// to avoid accidentally replacing "command" that appears in values. +func convertBashCommandToCmdField(argsRaw string) string { + // Only process valid JSON + if !gjson.Valid(argsRaw) { + return argsRaw + } + + // Check if "command" key exists and "cmd" doesn't + commandVal := gjson.Get(argsRaw, "command") + cmdVal := gjson.Get(argsRaw, "cmd") + + if commandVal.Exists() && !cmdVal.Exists() { + // Set "cmd" to the value of "command", preserve the raw value type + result, err := sjson.SetRaw(argsRaw, "cmd", commandVal.Raw) + if err != nil { + return argsRaw + } + // Delete "command" key + result, err = sjson.Delete(result, "command") + if err != nil { + return argsRaw + } + return result + } + + return argsRaw +} + // ConvertAntigravityResponseToClaudeNonStream converts a non-streaming Gemini CLI response to a non-streaming Claude response. // // Parameters: @@ -476,7 +511,12 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or toolBlock, _ = sjson.Set(toolBlock, "name", name) if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) { - toolBlock, _ = sjson.SetRaw(toolBlock, "input", args.Raw) + argsRaw := args.Raw + // Convert command → cmd for Bash tools + if name == "Bash" || name == "bash" || name == "bash_20241022" { + argsRaw = convertBashCommandToCmdField(argsRaw) + } + toolBlock, _ = sjson.SetRaw(toolBlock, "input", argsRaw) } ensureContentArray() diff --git a/internal/translator/antigravity/claude/antigravity_claude_response_test.go b/internal/translator/antigravity/claude/antigravity_claude_response_test.go index 7ffd7666..4c2f31c1 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_response_test.go +++ b/internal/translator/antigravity/claude/antigravity_claude_response_test.go @@ -82,7 +82,7 @@ func TestConvertBashCommandToCmdField(t *testing.T) { } // ============================================================================ -// P3: Signature Caching Tests +// Signature Caching Tests // ============================================================================ func TestConvertAntigravityResponseToClaude_SessionIDDerived(t *testing.T) { diff --git a/internal/util/gemini_schema_test.go b/internal/util/gemini_schema_test.go index 0f9e3eba..69adbcdb 100644 --- a/internal/util/gemini_schema_test.go +++ b/internal/util/gemini_schema_test.go @@ -631,7 +631,7 @@ func compareJSON(t *testing.T, expectedJSON, actualJSON string) { } // ============================================================================ -// P0-1: Empty Schema Placeholder Tests +// Empty Schema Placeholder Tests // ============================================================================ func TestCleanJSONSchemaForAntigravity_EmptySchemaPlaceholder(t *testing.T) { @@ -732,7 +732,7 @@ func TestCleanJSONSchemaForAntigravity_EmptySchemaWithDescription(t *testing.T) } // ============================================================================ -// P0-2: Format field handling (ad-hoc patch removal) +// Format field handling (ad-hoc patch removal) // ============================================================================ func TestCleanJSONSchemaForAntigravity_FormatFieldRemoval(t *testing.T) {