// Package claude provides response translation functionality for Claude Code API compatibility. // This package handles the conversion of backend client responses into Claude Code-compatible // Server-Sent Events (SSE) format, implementing a sophisticated state machine that manages // different response types including text content, thinking processes, and function calls. // The translation ensures proper sequencing of SSE events and maintains state across // multiple response chunks to provide a seamless streaming experience. package claude import ( "bytes" "context" "encoding/base64" "fmt" "strings" "sync/atomic" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" sigcompat "github.com/router-for-me/CLIProxyAPI/v7/internal/signature" translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/router-for-me/CLIProxyAPI/v7/internal/util" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) // decodeSignature decodes R... (2-layer Base64) to E... (1-layer Base64, Anthropic format). // Returns empty string if decoding fails (skip invalid signatures). func decodeSignature(signature string) string { if signature == "" { return signature } if strings.HasPrefix(signature, "R") { decoded, err := base64.StdEncoding.DecodeString(signature) if err != nil { log.Warnf("antigravity claude response: failed to decode signature, skipping") return "" } return string(decoded) } return signature } func formatGeminiClaudeCarrierValue(modelName, signature, direction, targetKind string) string { if sigcompat.SignatureProviderFromModelName(modelName) == sigcompat.SignatureProviderGemini { return encodeGeminiClaudeCarrierSignature(signature, direction, targetKind) } return formatClaudeSignatureValue(modelName, signature) } func formatClaudeSignatureValue(modelName, signature string) string { // Gemini signatures are provider-native replay state. Keep them raw so an // empty detached thinking block or tool_use block can round-trip through // Claude Code and be recognized by the Gemini request translator. if cache.GetModelGroup(modelName) == "gemini" { return signature } if cache.SignatureCacheEnabled() { return fmt.Sprintf("%s#%s", cache.GetModelGroup(modelName), signature) } if cache.GetModelGroup(modelName) == "claude" { return decodeSignature(signature) } return signature } // Params holds parameters for response conversion and maintains state across streaming chunks. // This structure tracks the current state of the response translation process to ensure // proper sequencing of SSE events and transitions between different content types. type Params struct { HasFirstResponse bool // Indicates if the initial message_start event has been sent ResponseType int // Current response type: 0=none, 1=content, 2=thinking, 3=function ResponseIndex int // Index counter for content blocks in the streaming response HasFinishReason bool // Tracks whether a finish reason has been observed FinishReason string // The finish reason string returned by the provider HasUsageMetadata bool // Tracks whether usage metadata has been observed PromptTokenCount int64 // Cached prompt token count from usage metadata CandidatesTokenCount int64 // Cached candidate token count from usage metadata ThoughtsTokenCount int64 // Cached thinking token count from usage metadata TotalTokenCount int64 // Cached total token count from usage metadata CachedTokenCount int64 // Cached content token count (indicates prompt caching) HasSentFinalEvents bool // Indicates if final content/message events have been sent 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 HasSemanticContent bool LastSemanticKind string HasWebSearchTool bool WebSearchRequests int64 WebSearchTextBuffer strings.Builder // Signature caching support CurrentThinkingText strings.Builder // Accumulates thinking text for signature caching CurrentThinkingSigned bool // Tracks whether the active thinking block already has its terminal signature // Reverse map: sanitized Gemini function name → original Claude tool name. // Populated lazily on the first response chunk from the original request JSON. ToolNameMap map[string]string } // toolUseIDCounter provides a process-wide unique counter for tool use identifiers. var toolUseIDCounter uint64 func antigravityClaudeToolUseID(modelName string, functionCall gjson.Result, fallback string) string { if sigcompat.SignatureProviderFromModelName(modelName) == sigcompat.SignatureProviderGemini { if stableID := util.GeminiClaudeToolUseID(functionCall.Get("id").String(), functionCall.Get("name").String(), functionCall.Get("args").Raw); stableID != "" { return stableID } } return util.SanitizeClaudeToolID(fallback) } // ConvertAntigravityResponseToClaude performs sophisticated streaming response format conversion. // This function implements a complex state machine that translates backend client responses // into Claude Code-compatible Server-Sent Events (SSE) format. It manages different response types // and handles state transitions between content blocks, thinking processes, and function calls. // // Response type states: 0=none, 1=content, 2=thinking, 3=function // The function maintains state across multiple calls to ensure proper SSE event sequencing. // // Parameters: // - ctx: The context for the request, used for cancellation and timeout handling // - modelName: The name of the model being used for the response (unused in current implementation) // - rawJSON: The raw JSON response from the Antigravity API // - param: A pointer to a parameter object for maintaining state between calls // // Returns: // - [][]byte: A slice of bytes, each containing a Claude Code-compatible SSE payload. func ConvertAntigravityResponseToClaude(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &Params{ HasFirstResponse: false, ResponseType: 0, ResponseIndex: 0, ToolNameMap: util.DisambiguatedToolNameMap(originalRequestRawJSON), } } modelName := gjson.GetBytes(requestRawJSON, "model").String() params := (*param).(*Params) if bytes.Equal(rawJSON, []byte("[DONE]")) { output := make([]byte, 0, 256) if params.HasFirstResponse && !params.HasContent { output = translatorcommon.AppendSSEEventString(output, "content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, params.ResponseIndex), 3) params.ResponseType = 1 params.HasContent = true } if params.HasContent { appendFinalEvents(params, &output, true) output = translatorcommon.AppendSSEEventString(output, "message_stop", `{"type":"message_stop"}`, 3) return [][]byte{output} } return [][]byte{} } output := make([]byte, 0, 1024) appendEvent := func(event, payload string) { output = translatorcommon.AppendSSEEventString(output, event, payload, 3) } webSearchStreamMode := shouldTranslateWebSearchGrounding(originalRequestRawJSON, requestRawJSON) appendThinkingSignature := func(signature, direction, targetKind string) { if signature == "" || params.ResponseType != 2 { return } if params.CurrentThinkingText.Len() > 0 { cache.CacheSignatureBestEffort(ctx, modelName, params.CurrentThinkingText.String(), signature) params.CurrentThinkingText.Reset() } sigValue := formatGeminiClaudeCarrierValue(modelName, signature, direction, targetKind) data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex)), "delta.signature", sigValue) appendEvent("content_block_delta", string(data)) params.CurrentThinkingSigned = true params.HasContent = true } closeCurrentBlock := func() { if params.ResponseType == 0 { return } appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex)) params.ResponseIndex++ params.ResponseType = 0 params.CurrentThinkingSigned = false } startEmptyThinkingBlock := func() { appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, params.ResponseIndex)) params.ResponseType = 2 params.CurrentThinkingSigned = false params.HasContent = true } appendCarrierSignature := func(signature, direction, targetKind string) { if signature == "" || params.ResponseType != 2 { return } sigValue := formatGeminiClaudeCarrierValue(modelName, signature, direction, targetKind) data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex)), "delta.signature", sigValue) appendEvent("content_block_delta", string(data)) params.CurrentThinkingSigned = true params.HasContent = true } appendPartSignature := func(signature, direction, targetKind string) bool { if signature == "" { return false } if params.ResponseType == 2 && !params.CurrentThinkingSigned { appendThinkingSignature(signature, direction, targetKind) return false } closeCurrentBlock() startEmptyThinkingBlock() appendCarrierSignature(signature, direction, targetKind) return true } // Initialize the streaming session with a message_start event // This is only sent for the very first response chunk to establish the streaming session if !params.HasFirstResponse { // Create the initial message structure with default values according to Claude Code API specification // This follows the Claude Code API specification for streaming message initialization messageStartTemplate := []byte(`{"type": "message_start", "message": {"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", "type": "message", "role": "assistant", "content": [], "model": "claude-3-5-sonnet-20241022", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 0, "output_tokens": 0}}}`) // Use cpaUsageMetadata within the message_start event for Claude. if promptTokenCount := gjson.GetBytes(rawJSON, "response.cpaUsageMetadata.promptTokenCount"); promptTokenCount.Exists() { messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.usage.input_tokens", promptTokenCount.Int()) } if candidatesTokenCount := gjson.GetBytes(rawJSON, "response.cpaUsageMetadata.candidatesTokenCount"); candidatesTokenCount.Exists() && !webSearchStreamMode { messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.usage.output_tokens", candidatesTokenCount.Int()) } // Override default values with actual response metadata if available from the Antigravity response if modelVersionResult := gjson.GetBytes(rawJSON, "response.modelVersion"); modelVersionResult.Exists() { messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.model", modelVersionResult.String()) } if responseIDResult := gjson.GetBytes(rawJSON, "response.responseId"); responseIDResult.Exists() { messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.id", responseIDResult.String()) } appendEvent("message_start", string(messageStartTemplate)) params.HasFirstResponse = true } handledWebSearchGrounding := false if webSearchStreamMode && !params.HasWebSearchTool { root := gjson.ParseBytes(rawJSON) if groundingMetadata := antigravityGroundingMetadata(root); groundingMetadata.Exists() { toolUseID := newClaudeWebSearchToolUseID() textContent := params.WebSearchTextBuffer.String() + antigravityTextContent(root) params.WebSearchTextBuffer.Reset() params.ResponseIndex = appendClaudeWebSearchStreamBlocks(appendEvent, params.ResponseIndex, toolUseID, textContent, groundingMetadata) params.HasWebSearchTool = true params.WebSearchRequests = 1 params.HasContent = true params.ResponseType = 0 handledWebSearchGrounding = true } } // Process the response parts array from the backend client // Each part can contain text content, thinking content, or function calls partsResult := gjson.GetBytes(rawJSON, "response.candidates.0.content.parts") if partsResult.IsArray() && webSearchStreamMode && !params.HasWebSearchTool && !handledWebSearchGrounding { appendWebSearchBufferedText(partsResult, ¶ms.WebSearchTextBuffer) } else if partsResult.IsArray() && !handledWebSearchGrounding { partResults := partsResult.Array() for i := 0; i < len(partResults); i++ { partResult := partResults[i] // Extract the different types of content from each part partTextResult := partResult.Get("text") functionCallResult := partResult.Get("functionCall") thoughtSignatureResult := partResult.Get("thoughtSignature") if !thoughtSignatureResult.Exists() { thoughtSignatureResult = partResult.Get("thought_signature") } hasThoughtSignature := thoughtSignatureResult.Exists() && thoughtSignatureResult.String() != "" && !functionCallResult.Exists() if hasThoughtSignature && (!partTextResult.Exists() || partTextResult.String() == "") { direction := geminiClaudeCarrierNext targetKind := geminiClaudeCarrierAny if params.HasSemanticContent { direction = geminiClaudeCarrierPrevious targetKind = params.LastSemanticKind } appendPartSignature(thoughtSignatureResult.String(), direction, targetKind) continue } // Handle text content (both regular content and thinking) if partTextResult.Exists() { partText := partTextResult.String() if partResult.Get("thought").Bool() { if partText != "" { params.HasSemanticContent = true params.LastSemanticKind = geminiClaudeCarrierText if params.ResponseType == 2 && params.CurrentThinkingSigned { closeCurrentBlock() } if params.ResponseType == 2 { params.CurrentThinkingText.WriteString(partText) data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex)), "delta.thinking", partText) appendEvent("content_block_delta", string(data)) params.HasContent = true } else { if params.ResponseType != 0 { appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex)) params.ResponseIndex++ } appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, params.ResponseIndex)) params.CurrentThinkingSigned = false data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex)), "delta.thinking", partText) appendEvent("content_block_delta", string(data)) params.ResponseType = 2 params.HasContent = true params.CurrentThinkingText.Reset() params.CurrentThinkingText.WriteString(partText) } } if hasThoughtSignature { appendThinkingSignature(thoughtSignatureResult.String(), geminiClaudeCarrierStandalone, geminiClaudeCarrierText) } } else { signatureTargetsVisibleText := false if hasThoughtSignature { signatureTargetsVisibleText = appendPartSignature(thoughtSignatureResult.String(), geminiClaudeCarrierNext, geminiClaudeCarrierText) } finishReasonResult := gjson.GetBytes(rawJSON, "response.candidates.0.finishReason") if partText != "" || !finishReasonResult.Exists() { if params.ResponseType == 1 { data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex)), "delta.text", partText) appendEvent("content_block_delta", string(data)) params.HasContent = true } else { if params.ResponseType != 0 { appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex)) params.ResponseIndex++ } if partText != "" { appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, params.ResponseIndex)) data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex)), "delta.text", partText) appendEvent("content_block_delta", string(data)) params.ResponseType = 1 params.HasContent = true } } } if partText != "" { params.HasSemanticContent = true params.LastSemanticKind = geminiClaudeCarrierText if signatureTargetsVisibleText { closeCurrentBlock() } } } } else if functionCallResult.Exists() { toolSignature := thoughtSignatureResult.String() if cache.GetModelGroup(modelName) != "claude" { appendPartSignature(toolSignature, geminiClaudeCarrierNext, geminiClaudeCarrierFunction) } // Handle function/tool calls from the AI model // This processes tool usage requests and formats them for Claude Code API compatibility params.HasToolUse = true fcName := util.RestoreSanitizedToolName(params.ToolNameMap, functionCallResult.Get("name").String()) // Handle state transitions when switching to function calls // Close any existing function call block first if params.ResponseType == 3 { appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex)) params.ResponseIndex++ params.ResponseType = 0 } // Special handling for thinking state transition if params.ResponseType == 2 { // output = output + "event: content_block_delta\n" // output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, params.ResponseIndex) // output = output + "\n\n\n" } // Close any other existing content block if params.ResponseType != 0 { appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex)) params.ResponseIndex++ } // Start a new tool use content block // This creates the structure for a function call in Claude Code format // Create the tool use block with unique ID and function details data := []byte(fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, params.ResponseIndex)) fallbackID := fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&toolUseIDCounter, 1)) data, _ = sjson.SetBytes(data, "content_block.id", antigravityClaudeToolUseID(modelName, functionCallResult, fallbackID)) data, _ = sjson.SetBytes(data, "content_block.name", fcName) if cache.GetModelGroup(modelName) == "claude" && toolSignature != "" { data, _ = sjson.SetBytes(data, "content_block.signature", formatClaudeSignatureValue(modelName, toolSignature)) } appendEvent("content_block_start", string(data)) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { data, _ = sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, params.ResponseIndex)), "delta.partial_json", fcArgsResult.Raw) appendEvent("content_block_delta", string(data)) } params.ResponseType = 3 params.HasContent = true params.HasSemanticContent = true params.LastSemanticKind = geminiClaudeCarrierFunction } } } if finishReasonResult := gjson.GetBytes(rawJSON, "response.candidates.0.finishReason"); finishReasonResult.Exists() { params.HasFinishReason = true params.FinishReason = finishReasonResult.String() } if usageResult := gjson.GetBytes(rawJSON, "response.usageMetadata"); usageResult.Exists() { params.HasUsageMetadata = true params.CachedTokenCount = usageResult.Get("cachedContentTokenCount").Int() params.PromptTokenCount = usageResult.Get("promptTokenCount").Int() - params.CachedTokenCount params.CandidatesTokenCount = usageResult.Get("candidatesTokenCount").Int() params.ThoughtsTokenCount = usageResult.Get("thoughtsTokenCount").Int() params.TotalTokenCount = usageResult.Get("totalTokenCount").Int() if params.CandidatesTokenCount == 0 && params.TotalTokenCount > 0 { params.CandidatesTokenCount = params.TotalTokenCount - params.PromptTokenCount - params.ThoughtsTokenCount if params.CandidatesTokenCount < 0 { params.CandidatesTokenCount = 0 } } } if webSearchStreamMode && !params.HasWebSearchTool && params.HasFinishReason && params.WebSearchTextBuffer.Len() > 0 { appendBufferedWebSearchTextBlock(params, appendEvent) } if params.HasUsageMetadata && params.HasFinishReason { appendFinalEvents(params, &output, false) } return [][]byte{output} } func appendWebSearchBufferedText(partsResult gjson.Result, buffer *strings.Builder) { for _, partResult := range partsResult.Array() { if partResult.Get("thought").Bool() || partResult.Get("functionCall").Exists() { continue } if partTextResult := partResult.Get("text"); partTextResult.Exists() { buffer.WriteString(partTextResult.String()) } } } func appendBufferedWebSearchTextBlock(params *Params, appendEvent func(string, string)) { text := params.WebSearchTextBuffer.String() params.WebSearchTextBuffer.Reset() if text == "" { return } appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, params.ResponseIndex)) data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex)), "delta.text", text) appendEvent("content_block_delta", string(data)) params.ResponseType = 1 params.HasContent = true } func appendFinalEvents(params *Params, output *[]byte, force bool) { if params.HasSentFinalEvents { return } if !params.HasUsageMetadata && !force { return } // Only send final events if we have actually output content if !params.HasContent { return } if params.ResponseType != 0 { *output = translatorcommon.AppendSSEEventString(*output, "content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex), 3) params.ResponseType = 0 } stopReason := resolveStopReason(params) usageOutputTokens := params.CandidatesTokenCount + params.ThoughtsTokenCount if usageOutputTokens == 0 && params.TotalTokenCount > 0 { usageOutputTokens = params.TotalTokenCount - params.PromptTokenCount if usageOutputTokens < 0 { usageOutputTokens = 0 } } delta := []byte(fmt.Sprintf(`{"type":"message_delta","delta":{"stop_reason":"%s","stop_sequence":null},"usage":{"input_tokens":%d,"output_tokens":%d}}`, stopReason, params.PromptTokenCount, usageOutputTokens)) if params.WebSearchRequests > 0 { delta, _ = sjson.SetBytes(delta, "usage.server_tool_use.web_search_requests", params.WebSearchRequests) } // Add cache_read_input_tokens if cached tokens are present (indicates prompt caching is working) if params.CachedTokenCount > 0 { var err error delta, err = sjson.SetBytes(delta, "usage.cache_read_input_tokens", params.CachedTokenCount) if err != nil { log.Warnf("antigravity claude response: failed to set cache_read_input_tokens: %v", err) } } *output = translatorcommon.AppendSSEEventString(*output, "message_delta", string(delta), 3) params.HasSentFinalEvents = true } func resolveStopReason(params *Params) string { if params.HasToolUse { return "tool_use" } switch params.FinishReason { case "MAX_TOKENS": return "max_tokens" case "STOP", "FINISH_REASON_UNSPECIFIED", "UNKNOWN": return "end_turn" } return "end_turn" } // ConvertAntigravityResponseToClaudeNonStream converts a non-streaming Antigravity response to a non-streaming Claude response. // // Parameters: // - ctx: The context for the request. // - modelName: The name of the model. // - rawJSON: The raw JSON response from the Antigravity API. // - param: A pointer to a parameter object for the conversion. // // Returns: // - []byte: A Claude-compatible JSON response. func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { toolNameMap := util.DisambiguatedToolNameMap(originalRequestRawJSON) modelName := gjson.GetBytes(requestRawJSON, "model").String() root := gjson.ParseBytes(rawJSON) promptTokens := root.Get("response.usageMetadata.promptTokenCount").Int() candidateTokens := root.Get("response.usageMetadata.candidatesTokenCount").Int() thoughtTokens := root.Get("response.usageMetadata.thoughtsTokenCount").Int() totalTokens := root.Get("response.usageMetadata.totalTokenCount").Int() cachedTokens := root.Get("response.usageMetadata.cachedContentTokenCount").Int() outputTokens := candidateTokens + thoughtTokens if outputTokens == 0 && totalTokens > 0 { outputTokens = totalTokens - promptTokens if outputTokens < 0 { outputTokens = 0 } } responseJSON := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`) responseJSON, _ = sjson.SetBytes(responseJSON, "id", root.Get("response.responseId").String()) responseJSON, _ = sjson.SetBytes(responseJSON, "model", root.Get("response.modelVersion").String()) responseJSON, _ = sjson.SetBytes(responseJSON, "usage.input_tokens", promptTokens) responseJSON, _ = sjson.SetBytes(responseJSON, "usage.output_tokens", outputTokens) // Add cache_read_input_tokens if cached tokens are present (indicates prompt caching is working) if cachedTokens > 0 { var err error responseJSON, err = sjson.SetBytes(responseJSON, "usage.cache_read_input_tokens", cachedTokens) if err != nil { log.Warnf("antigravity claude response: failed to set cache_read_input_tokens: %v", err) } } if shouldTranslateWebSearchGrounding(originalRequestRawJSON, requestRawJSON) { if groundingMetadata := antigravityGroundingMetadata(root); groundingMetadata.Exists() { toolUseID := newClaudeWebSearchToolUseID() responseJSON, _ = sjson.SetRawBytes(responseJSON, "content", buildClaudeWebSearchContent(toolUseID, antigravityTextContent(root), groundingMetadata)) responseJSON, _ = sjson.SetBytes(responseJSON, "stop_reason", "end_turn") responseJSON, _ = sjson.SetBytes(responseJSON, "usage.server_tool_use.web_search_requests", 1) return responseJSON } } var blocks [][]byte parts := root.Get("response.candidates.0.content.parts") textBuilder := strings.Builder{} thinkingBuilder := strings.Builder{} thinkingSignature := "" thinkingSignatureDirection := geminiClaudeCarrierStandalone thinkingSignatureTargetKind := geminiClaudeCarrierText toolIDCounter := 0 hasToolCall := false hasSemanticContent := false lastSemanticKind := geminiClaudeCarrierAny flushText := func() { if textBuilder.Len() == 0 { return } block := []byte(`{"type":"text","text":""}`) block, _ = sjson.SetBytes(block, "text", textBuilder.String()) blocks = append(blocks, block) textBuilder.Reset() } flushThinking := func() { if thinkingBuilder.Len() == 0 && thinkingSignature == "" { return } block := []byte(`{"type":"thinking","thinking":""}`) block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String()) if thinkingSignature != "" { sigValue := formatGeminiClaudeCarrierValue(modelName, thinkingSignature, thinkingSignatureDirection, thinkingSignatureTargetKind) block, _ = sjson.SetBytes(block, "signature", sigValue) } blocks = append(blocks, block) thinkingBuilder.Reset() thinkingSignature = "" thinkingSignatureDirection = geminiClaudeCarrierStandalone thinkingSignatureTargetKind = geminiClaudeCarrierText } appendSignatureCarrier := func(signature, direction, targetKind string) { if signature == "" { return } carrier := []byte(`{"type":"thinking","thinking":"","signature":""}`) carrier, _ = sjson.SetBytes(carrier, "signature", formatGeminiClaudeCarrierValue(modelName, signature, direction, targetKind)) blocks = append(blocks, carrier) } if parts.IsArray() { for _, part := range parts.Array() { sig := part.Get("thoughtSignature") if !sig.Exists() { sig = part.Get("thought_signature") } signature := "" if sig.Exists() { signature = sig.String() } if functionCall := part.Get("functionCall"); functionCall.Exists() { signatureAttachedToThought := false isClaudeTarget := cache.GetModelGroup(modelName) == "claude" if !isClaudeTarget && signature != "" && thinkingBuilder.Len() > 0 && thinkingSignature == "" { thinkingSignature = signature thinkingSignatureDirection = geminiClaudeCarrierNext thinkingSignatureTargetKind = geminiClaudeCarrierFunction signatureAttachedToThought = true } flushThinking() flushText() hasToolCall = true name := util.RestoreSanitizedToolName(toolNameMap, functionCall.Get("name").String()) toolIDCounter++ if !isClaudeTarget && signature != "" && !signatureAttachedToThought { appendSignatureCarrier(signature, geminiClaudeCarrierNext, geminiClaudeCarrierFunction) } toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) toolBlock, _ = sjson.SetBytes(toolBlock, "id", antigravityClaudeToolUseID(modelName, functionCall, fmt.Sprintf("tool_%d", toolIDCounter))) toolBlock, _ = sjson.SetBytes(toolBlock, "name", name) if isClaudeTarget && signature != "" { toolBlock, _ = sjson.SetBytes(toolBlock, "signature", formatClaudeSignatureValue(modelName, signature)) } if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) && args.IsObject() { toolBlock, _ = sjson.SetRawBytes(toolBlock, "input", []byte(args.Raw)) } blocks = append(blocks, toolBlock) hasSemanticContent = true lastSemanticKind = geminiClaudeCarrierFunction continue } text := part.Get("text") isThought := part.Get("thought").Bool() if isThought { flushText() if thinkingSignature != "" { flushThinking() } if text.Exists() && text.String() != "" { thinkingBuilder.WriteString(text.String()) hasSemanticContent = true lastSemanticKind = geminiClaudeCarrierText } if signature != "" { if thinkingBuilder.Len() > 0 { thinkingSignature = signature thinkingSignatureDirection = geminiClaudeCarrierStandalone thinkingSignatureTargetKind = geminiClaudeCarrierText flushThinking() } else if hasSemanticContent { appendSignatureCarrier(signature, geminiClaudeCarrierPrevious, lastSemanticKind) } else { appendSignatureCarrier(signature, geminiClaudeCarrierNext, geminiClaudeCarrierAny) } } continue } visibleSignatureCarrier := false if signature != "" { if thinkingBuilder.Len() > 0 && thinkingSignature == "" { thinkingSignature = signature thinkingSignatureDirection = geminiClaudeCarrierNext thinkingSignatureTargetKind = geminiClaudeCarrierText flushThinking() } else { flushThinking() flushText() if text.Exists() && text.String() != "" { appendSignatureCarrier(signature, geminiClaudeCarrierNext, geminiClaudeCarrierText) visibleSignatureCarrier = true } else if hasSemanticContent { appendSignatureCarrier(signature, geminiClaudeCarrierPrevious, lastSemanticKind) } else { appendSignatureCarrier(signature, geminiClaudeCarrierNext, geminiClaudeCarrierAny) } } } if text.Exists() && text.String() != "" { flushThinking() textBuilder.WriteString(text.String()) hasSemanticContent = true lastSemanticKind = geminiClaudeCarrierText if visibleSignatureCarrier { flushText() } } } } flushThinking() flushText() if len(blocks) > 0 { responseJSON, _ = sjson.SetRawBytes(responseJSON, "content", translatorcommon.JoinRawArray(blocks)) } stopReason := "end_turn" if hasToolCall { stopReason = "tool_use" } else { if finish := root.Get("response.candidates.0.finishReason"); finish.Exists() { switch finish.String() { case "MAX_TOKENS": stopReason = "max_tokens" case "STOP", "FINISH_REASON_UNSPECIFIED", "UNKNOWN": stopReason = "end_turn" default: stopReason = "end_turn" } } } responseJSON, _ = sjson.SetBytes(responseJSON, "stop_reason", stopReason) if promptTokens == 0 && outputTokens == 0 { if usageMeta := root.Get("response.usageMetadata"); !usageMeta.Exists() { responseJSON, _ = sjson.DeleteBytes(responseJSON, "usage") } } return responseJSON } func ClaudeTokenCount(ctx context.Context, count int64) []byte { return translatorcommon.ClaudeInputTokensJSON(count) }