From 08586334af6655f681b3da92b353b810a3478ee4 Mon Sep 17 00:00:00 2001 From: "huynguyen03.dev" Date: Sat, 6 Dec 2025 18:59:44 +0700 Subject: [PATCH 1/6] fix(amp): pass mapped model to gemini bridge via context Gemini handler extracts model from URL path, not JSON body, so rewriting the request body alone wasn't sufficient for model mapping. - Add MappedModelContextKey constant for context passing - Update routes.go to use NewFallbackHandlerWithMapper - Add check for valid mapping before routing to local handler - Add tests for gemini bridge model mapping --- internal/api/modules/amp/fallback_handlers.go | 5 + internal/api/modules/amp/gemini_bridge.go | 12 ++ .../api/modules/amp/gemini_bridge_test.go | 120 ++++++++++++++++++ internal/api/modules/amp/routes.go | 16 ++- 4 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 internal/api/modules/amp/gemini_bridge_test.go diff --git a/internal/api/modules/amp/fallback_handlers.go b/internal/api/modules/amp/fallback_handlers.go index b38ab5f1..0cbe0e1a 100644 --- a/internal/api/modules/amp/fallback_handlers.go +++ b/internal/api/modules/amp/fallback_handlers.go @@ -28,6 +28,9 @@ const ( RouteTypeNoProvider AmpRouteType = "NO_PROVIDER" ) +// MappedModelContextKey is the Gin context key for passing mapped model names. +const MappedModelContextKey = "mapped_model" + // logAmpRouting logs the routing decision for an Amp request with structured fields func logAmpRouting(routeType AmpRouteType, requestedModel, resolvedModel, provider, path string) { fields := log.Fields{ @@ -141,6 +144,8 @@ func (fh *FallbackHandler) WrapHandler(handler gin.HandlerFunc) gin.HandlerFunc // Mapping found - rewrite the model in request body bodyBytes = rewriteModelInRequest(bodyBytes, mappedModel) c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + // Store mapped model in context for handlers that check it (like gemini bridge) + c.Set(MappedModelContextKey, mappedModel) resolvedModel = mappedModel usedMapping = true diff --git a/internal/api/modules/amp/gemini_bridge.go b/internal/api/modules/amp/gemini_bridge.go index 3b3d8374..cc31b685 100644 --- a/internal/api/modules/amp/gemini_bridge.go +++ b/internal/api/modules/amp/gemini_bridge.go @@ -26,6 +26,18 @@ func createGeminiBridgeHandler(geminiHandler *gemini.GeminiAPIHandler) gin.Handl // Extract everything after "/models/" actionPart := path[idx+8:] // Skip "/models/" + // Check if model was mapped by FallbackHandler + if mappedModel, exists := c.Get(MappedModelContextKey); exists { + if strModel, ok := mappedModel.(string); ok && strModel != "" { + // Replace the model part in the action + // actionPart is like "model-name:method" + if colonIdx := strings.Index(actionPart, ":"); colonIdx > 0 { + method := actionPart[colonIdx:] // ":method" + actionPart = strModel + method + } + } + } + // Set this as the :action parameter that the Gemini handler expects c.Params = append(c.Params, gin.Param{ Key: "action", diff --git a/internal/api/modules/amp/gemini_bridge_test.go b/internal/api/modules/amp/gemini_bridge_test.go new file mode 100644 index 00000000..88accbf4 --- /dev/null +++ b/internal/api/modules/amp/gemini_bridge_test.go @@ -0,0 +1,120 @@ +package amp + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini" +) + +func TestCreateGeminiBridgeHandler_ActionParameterExtraction(t *testing.T) { + gin.SetMode(gin.TestMode) + + tests := []struct { + name string + path string + mappedModel string // empty string means no mapping + expectedAction string + }{ + { + name: "no_mapping_uses_url_model", + path: "/publishers/google/models/gemini-pro:generateContent", + mappedModel: "", + expectedAction: "gemini-pro:generateContent", + }, + { + name: "mapped_model_replaces_url_model", + path: "/publishers/google/models/gemini-exp:generateContent", + mappedModel: "gemini-2.0-flash", + expectedAction: "gemini-2.0-flash:generateContent", + }, + { + name: "mapping_preserves_method", + path: "/publishers/google/models/gemini-2.5-preview:streamGenerateContent", + mappedModel: "gemini-flash", + expectedAction: "gemini-flash:streamGenerateContent", + }, + { + name: "empty_mapped_model_ignored", + path: "/publishers/google/models/gemini-pro:generateContent", + mappedModel: "", + expectedAction: "gemini-pro:generateContent", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var capturedAction string + + mockGeminiHandler := func(c *gin.Context) { + capturedAction = c.Param("action") + c.JSON(http.StatusOK, gin.H{"captured": capturedAction}) + } + + // Mirror the bridge logic from gemini_bridge.go + bridgeHandler := func(c *gin.Context) { + path := c.Param("path") + if idx := strings.Index(path, "/models/"); idx >= 0 { + actionPart := path[idx+8:] + + if mappedModel, exists := c.Get(MappedModelContextKey); exists { + if strModel, ok := mappedModel.(string); ok && strModel != "" { + if colonIdx := strings.Index(actionPart, ":"); colonIdx > 0 { + method := actionPart[colonIdx:] + actionPart = strModel + method + } + } + } + + c.Params = append(c.Params, gin.Param{Key: "action", Value: actionPart}) + mockGeminiHandler(c) + return + } + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid path"}) + } + + r := gin.New() + if tt.mappedModel != "" { + r.Use(func(c *gin.Context) { + c.Set(MappedModelContextKey, tt.mappedModel) + c.Next() + }) + } + r.POST("/api/provider/google/v1beta1/*path", bridgeHandler) + + req := httptest.NewRequest(http.MethodPost, "/api/provider/google/v1beta1"+tt.path, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("Expected status 200, got %d", w.Code) + } + if capturedAction != tt.expectedAction { + t.Errorf("Expected action '%s', got '%s'", tt.expectedAction, capturedAction) + } + }) + } +} + +func TestCreateGeminiBridgeHandler_InvalidPath(t *testing.T) { + gin.SetMode(gin.TestMode) + + base := &handlers.BaseAPIHandler{} + geminiHandlers := gemini.NewGeminiAPIHandler(base) + bridgeHandler := createGeminiBridgeHandler(geminiHandlers) + + r := gin.New() + r.POST("/api/provider/google/v1beta1/*path", bridgeHandler) + + req := httptest.NewRequest(http.MethodPost, "/api/provider/google/v1beta1/invalid/path", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status 400 for invalid path, got %d", w.Code) + } +} diff --git a/internal/api/modules/amp/routes.go b/internal/api/modules/amp/routes.go index 57bb5246..1f61d5ac 100644 --- a/internal/api/modules/amp/routes.go +++ b/internal/api/modules/amp/routes.go @@ -170,9 +170,9 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha // If no local OAuth is available, falls back to ampcode.com proxy. geminiHandlers := gemini.NewGeminiAPIHandler(baseHandler) geminiBridge := createGeminiBridgeHandler(geminiHandlers) - geminiV1Beta1Fallback := NewFallbackHandler(func() *httputil.ReverseProxy { + geminiV1Beta1Fallback := NewFallbackHandlerWithMapper(func() *httputil.ReverseProxy { return m.getProxy() - }) + }, m.modelMapper) geminiV1Beta1Handler := geminiV1Beta1Fallback.WrapHandler(geminiBridge) // Route POST model calls through Gemini bridge when a local provider exists, otherwise proxy. @@ -187,8 +187,18 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha } if modelPart != "" { normalized, _ := util.NormalizeGeminiThinkingModel(modelPart) - // Only handle locally when we have a provider; otherwise fall back to proxy + // Only handle locally when we have a provider or a valid mapping; otherwise fall back to proxy + hasProvider := false if providers := util.GetProviderName(normalized); len(providers) > 0 { + hasProvider = true + } else if m.modelMapper != nil { + // Check if mapped model has provider (MapModel returns target only if it has providers) + if mapped := m.modelMapper.MapModel(normalized); mapped != "" { + hasProvider = true + } + } + + if hasProvider { geminiV1Beta1Handler(c) return } From edc654edf9a0fd6886b666fdc04a96bd65784719 Mon Sep 17 00:00:00 2001 From: "huynguyen03.dev" Date: Sat, 6 Dec 2025 22:07:40 +0700 Subject: [PATCH 2/6] refactor: simplify provider check logic in amp routes Amp-Thread-ID: https://ampcode.com/threads/T-a18fd71c-32ce-4c29-93d7-09f082740e51 --- internal/api/modules/amp/routes.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/internal/api/modules/amp/routes.go b/internal/api/modules/amp/routes.go index 1f61d5ac..33525e61 100644 --- a/internal/api/modules/amp/routes.go +++ b/internal/api/modules/amp/routes.go @@ -188,14 +188,10 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha if modelPart != "" { normalized, _ := util.NormalizeGeminiThinkingModel(modelPart) // Only handle locally when we have a provider or a valid mapping; otherwise fall back to proxy - hasProvider := false - if providers := util.GetProviderName(normalized); len(providers) > 0 { - hasProvider = true - } else if m.modelMapper != nil { + hasProvider := len(util.GetProviderName(normalized)) > 0 + if !hasProvider && m.modelMapper != nil { // Check if mapped model has provider (MapModel returns target only if it has providers) - if mapped := m.modelMapper.MapModel(normalized); mapped != "" { - hasProvider = true - } + hasProvider = m.modelMapper.MapModel(normalized) != "" } if hasProvider { From 396899a5305fc5129b0c0ff7076ad59d0c911b22 Mon Sep 17 00:00:00 2001 From: "huynguyen03.dev" Date: Sun, 7 Dec 2025 10:15:42 +0700 Subject: [PATCH 3/6] refactor: improve gemini bridge testability and code quality - Change createGeminiBridgeHandler to accept gin.HandlerFunc instead of *gemini.GeminiAPIHandler This allows tests to inject mock handlers instead of duplicating bridge logic - Replace magic number 8 with len(modelsPrefix) for better maintainability - Remove redundant test case that doesn't test edge case in production - Update routes.go to pass geminiHandlers.GeminiHandler directly Addresses PR review feedback on test architecture and code clarity. Amp-Thread-ID: https://ampcode.com/threads/T-1ae2c691-e434-4b99-a49a-10cabd3544db --- internal/api/modules/amp/gemini_bridge.go | 16 ++++---- .../api/modules/amp/gemini_bridge_test.go | 39 +++---------------- internal/api/modules/amp/routes.go | 2 +- 3 files changed, 16 insertions(+), 41 deletions(-) diff --git a/internal/api/modules/amp/gemini_bridge.go b/internal/api/modules/amp/gemini_bridge.go index cc31b685..d6ad8f79 100644 --- a/internal/api/modules/amp/gemini_bridge.go +++ b/internal/api/modules/amp/gemini_bridge.go @@ -4,7 +4,6 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini" ) // createGeminiBridgeHandler creates a handler that bridges AMP CLI's non-standard Gemini paths @@ -15,16 +14,19 @@ import ( // // This extracts the model+method from the AMP path and sets it as the :action parameter // so the standard Gemini handler can process it. -func createGeminiBridgeHandler(geminiHandler *gemini.GeminiAPIHandler) gin.HandlerFunc { +// +// The handler parameter should be a Gemini-compatible handler that expects the :action param. +func createGeminiBridgeHandler(handler gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { // Get the full path from the catch-all parameter path := c.Param("path") // Extract model:method from AMP CLI path format // Example: /publishers/google/models/gemini-3-pro-preview:streamGenerateContent - if idx := strings.Index(path, "/models/"); idx >= 0 { - // Extract everything after "/models/" - actionPart := path[idx+8:] // Skip "/models/" + const modelsPrefix = "/models/" + if idx := strings.Index(path, modelsPrefix); idx >= 0 { + // Extract everything after modelsPrefix + actionPart := path[idx+len(modelsPrefix):] // Check if model was mapped by FallbackHandler if mappedModel, exists := c.Get(MappedModelContextKey); exists { @@ -44,8 +46,8 @@ func createGeminiBridgeHandler(geminiHandler *gemini.GeminiAPIHandler) gin.Handl Value: actionPart, }) - // Call the standard Gemini handler - geminiHandler.GeminiHandler(c) + // Call the handler + handler(c) return } diff --git a/internal/api/modules/amp/gemini_bridge_test.go b/internal/api/modules/amp/gemini_bridge_test.go index 88accbf4..347456c3 100644 --- a/internal/api/modules/amp/gemini_bridge_test.go +++ b/internal/api/modules/amp/gemini_bridge_test.go @@ -3,12 +3,9 @@ package amp import ( "net/http" "net/http/httptest" - "strings" "testing" "github.com/gin-gonic/gin" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" - "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini" ) func TestCreateGeminiBridgeHandler_ActionParameterExtraction(t *testing.T) { @@ -38,12 +35,6 @@ func TestCreateGeminiBridgeHandler_ActionParameterExtraction(t *testing.T) { mappedModel: "gemini-flash", expectedAction: "gemini-flash:streamGenerateContent", }, - { - name: "empty_mapped_model_ignored", - path: "/publishers/google/models/gemini-pro:generateContent", - mappedModel: "", - expectedAction: "gemini-pro:generateContent", - }, } for _, tt := range tests { @@ -55,27 +46,8 @@ func TestCreateGeminiBridgeHandler_ActionParameterExtraction(t *testing.T) { c.JSON(http.StatusOK, gin.H{"captured": capturedAction}) } - // Mirror the bridge logic from gemini_bridge.go - bridgeHandler := func(c *gin.Context) { - path := c.Param("path") - if idx := strings.Index(path, "/models/"); idx >= 0 { - actionPart := path[idx+8:] - - if mappedModel, exists := c.Get(MappedModelContextKey); exists { - if strModel, ok := mappedModel.(string); ok && strModel != "" { - if colonIdx := strings.Index(actionPart, ":"); colonIdx > 0 { - method := actionPart[colonIdx:] - actionPart = strModel + method - } - } - } - - c.Params = append(c.Params, gin.Param{Key: "action", Value: actionPart}) - mockGeminiHandler(c) - return - } - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid path"}) - } + // Use the actual createGeminiBridgeHandler function + bridgeHandler := createGeminiBridgeHandler(mockGeminiHandler) r := gin.New() if tt.mappedModel != "" { @@ -103,9 +75,10 @@ func TestCreateGeminiBridgeHandler_ActionParameterExtraction(t *testing.T) { func TestCreateGeminiBridgeHandler_InvalidPath(t *testing.T) { gin.SetMode(gin.TestMode) - base := &handlers.BaseAPIHandler{} - geminiHandlers := gemini.NewGeminiAPIHandler(base) - bridgeHandler := createGeminiBridgeHandler(geminiHandlers) + mockHandler := func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"ok": true}) + } + bridgeHandler := createGeminiBridgeHandler(mockHandler) r := gin.New() r.POST("/api/provider/google/v1beta1/*path", bridgeHandler) diff --git a/internal/api/modules/amp/routes.go b/internal/api/modules/amp/routes.go index 33525e61..8cb208fe 100644 --- a/internal/api/modules/amp/routes.go +++ b/internal/api/modules/amp/routes.go @@ -169,7 +169,7 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha // We bridge these to our standard Gemini handler to enable local OAuth. // If no local OAuth is available, falls back to ampcode.com proxy. geminiHandlers := gemini.NewGeminiAPIHandler(baseHandler) - geminiBridge := createGeminiBridgeHandler(geminiHandlers) + geminiBridge := createGeminiBridgeHandler(geminiHandlers.GeminiHandler) geminiV1Beta1Fallback := NewFallbackHandlerWithMapper(func() *httputil.ReverseProxy { return m.getProxy() }, m.modelMapper) From bfc738b76a50e7fb2d9d7873888e48da954a7f65 Mon Sep 17 00:00:00 2001 From: "huynguyen03.dev" Date: Sun, 7 Dec 2025 10:54:58 +0700 Subject: [PATCH 4/6] refactor: remove duplicate provider check in gemini v1beta1 route Simplifies routing logic by delegating all provider/mapping/proxy decisions to FallbackHandler. Previously, the route checked for provider/mapping availability before calling the handler, then FallbackHandler performed the same checks again. Changes: - Remove model extraction and provider checking from route (lines 182-201) - Route now only checks if request is POST with /models/ path - FallbackHandler handles provider -> mapping -> proxy fallback - Remove unused internal/util import Benefits: - Eliminates duplicate checks (addresses PR review feedback #2) - Centralizes all provider/mapping logic in FallbackHandler - Reduces routing code by ~20 lines - Aligns with how other /api/provider routes work Performance: No impact (checks still happen once in FallbackHandler) --- internal/api/modules/amp/routes.go | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/internal/api/modules/amp/routes.go b/internal/api/modules/amp/routes.go index 8cb208fe..6826dbbe 100644 --- a/internal/api/modules/amp/routes.go +++ b/internal/api/modules/amp/routes.go @@ -9,7 +9,6 @@ import ( "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/claude" "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini" @@ -175,30 +174,16 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha }, m.modelMapper) geminiV1Beta1Handler := geminiV1Beta1Fallback.WrapHandler(geminiBridge) - // Route POST model calls through Gemini bridge when a local provider exists, otherwise proxy. + // Route POST model calls through Gemini bridge with FallbackHandler. + // FallbackHandler checks provider -> mapping -> proxy fallback automatically. // All other methods (e.g., GET model listing) always proxy to upstream to preserve Amp CLI behavior. ampAPI.Any("/provider/google/v1beta1/*path", func(c *gin.Context) { if c.Request.Method == "POST" { - // Attempt to extract the model name from the AMP-style path if path := c.Param("path"); strings.Contains(path, "/models/") { - modelPart := path[strings.Index(path, "/models/")+len("/models/"):] - if colonIdx := strings.Index(modelPart, ":"); colonIdx > 0 { - modelPart = modelPart[:colonIdx] - } - if modelPart != "" { - normalized, _ := util.NormalizeGeminiThinkingModel(modelPart) - // Only handle locally when we have a provider or a valid mapping; otherwise fall back to proxy - hasProvider := len(util.GetProviderName(normalized)) > 0 - if !hasProvider && m.modelMapper != nil { - // Check if mapped model has provider (MapModel returns target only if it has providers) - hasProvider = m.modelMapper.MapModel(normalized) != "" - } - - if hasProvider { - geminiV1Beta1Handler(c) - return - } - } + // POST with /models/ path -> use Gemini bridge with fallback handler + // FallbackHandler will check provider/mapping and proxy if needed + geminiV1Beta1Handler(c) + return } } // Non-POST or no local provider available -> proxy upstream From e54d2f6b2aeb103134bd12f79b2a54a3c846d256 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:49:14 +0800 Subject: [PATCH 5/6] feat(logging): add version info to request log output --- internal/logging/request_logger.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/logging/request_logger.go b/internal/logging/request_logger.go index eb31bfa9..c574febb 100644 --- a/internal/logging/request_logger.go +++ b/internal/logging/request_logger.go @@ -20,6 +20,7 @@ import ( "github.com/klauspost/compress/zstd" log "github.com/sirupsen/logrus" + "github.com/router-for-me/CLIProxyAPI/v6/internal/buildinfo" "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" ) @@ -603,6 +604,7 @@ func (l *FileRequestLogger) formatRequestInfo(url, method string, headers map[st var content strings.Builder content.WriteString("=== REQUEST INFO ===\n") + content.WriteString(fmt.Sprintf("Version: %s\n", buildinfo.Version)) content.WriteString(fmt.Sprintf("URL: %s\n", url)) content.WriteString(fmt.Sprintf("Method: %s\n", method)) content.WriteString(fmt.Sprintf("Timestamp: %s\n", time.Now().Format(time.RFC3339Nano))) From 76af4540341e227658b4e437dda7a28af6a14461 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sun, 7 Dec 2025 13:19:12 +0800 Subject: [PATCH 6/6] **feat(antigravity): enhance handling of "thinking" content and refine Claude model response processing** --- internal/interfaces/client_models.go | 2 ++ .../claude/antigravity_claude_request.go | 24 +++++++++++++---- .../claude/antigravity_claude_response.go | 26 +++++++++++-------- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/internal/interfaces/client_models.go b/internal/interfaces/client_models.go index df8cc0b0..c6e4ff78 100644 --- a/internal/interfaces/client_models.go +++ b/internal/interfaces/client_models.go @@ -56,6 +56,8 @@ type Content struct { // Part represents a distinct piece of content within a message. // A part can be text, inline data (like an image), a function call, or a function response. type Part struct { + Thought bool `json:"thought,omitempty"` + // Text contains plain text content. Text string `json:"text,omitempty"` diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index dfc4d893..e1b73da0 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -83,7 +83,15 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ for j := 0; j < len(contentResults); j++ { contentResult := contentResults[j] contentTypeResult := contentResult.Get("type") - if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" { + if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "thinking" { + prompt := contentResult.Get("thinking").String() + signatureResult := contentResult.Get("signature") + signature := geminiCLIClaudeThoughtSignature + if signatureResult.Exists() { + signature = signatureResult.String() + } + clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt, Thought: true, ThoughtSignature: signature}) + } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" { prompt := contentResult.Get("text").String() clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt}) } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" { @@ -92,10 +100,16 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ functionID := contentResult.Get("id").String() var args map[string]any if err := json.Unmarshal([]byte(functionArgs), &args); err == nil { - clientContent.Parts = append(clientContent.Parts, client.Part{ - FunctionCall: &client.FunctionCall{ID: functionID, Name: functionName, Args: args}, - ThoughtSignature: geminiCLIClaudeThoughtSignature, - }) + if strings.Contains(modelName, "claude") { + clientContent.Parts = append(clientContent.Parts, client.Part{ + FunctionCall: &client.FunctionCall{ID: functionID, Name: functionName, Args: args}, + }) + } else { + clientContent.Parts = append(clientContent.Parts, client.Part{ + FunctionCall: &client.FunctionCall{ID: functionID, Name: functionName, Args: args}, + ThoughtSignature: geminiCLIClaudeThoughtSignature, + }) + } } } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" { toolCallID := contentResult.Get("tool_use_id").String() diff --git a/internal/translator/antigravity/claude/antigravity_claude_response.go b/internal/translator/antigravity/claude/antigravity_claude_response.go index dd27a366..4073f20b 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_response.go +++ b/internal/translator/antigravity/claude/antigravity_claude_response.go @@ -111,8 +111,11 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq if partTextResult.Exists() { // Process thinking content (internal reasoning) if partResult.Get("thought").Bool() { - // Continue existing thinking block if already in thinking state - if params.ResponseType == 2 { + if thoughtSignature := partResult.Get("thoughtSignature"); thoughtSignature.Exists() && thoughtSignature.String() != "" { + output = output + "event: content_block_delta\n" + data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex), "delta.signature", thoughtSignature.String()) + output = output + fmt.Sprintf("data: %s\n\n\n", data) + } else if params.ResponseType == 2 { // Continue existing thinking block if already in thinking state output = output + "event: content_block_delta\n" data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex), "delta.thinking", partTextResult.String()) output = output + fmt.Sprintf("data: %s\n\n\n", data) @@ -163,15 +166,16 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq output = output + "\n\n\n" params.ResponseIndex++ } - - // Start a new text content block - output = output + "event: content_block_start\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, params.ResponseIndex) - output = output + "\n\n\n" - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex), "delta.text", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) - params.ResponseType = 1 // Set state to content + if partTextResult.String() != "" { + // Start a new text content block + output = output + "event: content_block_start\n" + output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, params.ResponseIndex) + output = output + "\n\n\n" + output = output + "event: content_block_delta\n" + data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex), "delta.text", partTextResult.String()) + output = output + fmt.Sprintf("data: %s\n\n\n", data) + params.ResponseType = 1 // Set state to content + } } } }