diff --git a/internal/interfaces/client_models.go b/internal/interfaces/client_models.go index 885b471a..df8cc0b0 100644 --- a/internal/interfaces/client_models.go +++ b/internal/interfaces/client_models.go @@ -85,6 +85,9 @@ type InlineData struct { // FunctionCall represents a tool call requested by the model. // It includes the function name and its arguments that the model wants to execute. type FunctionCall struct { + // ID is the identifier of the function to be called. + ID string `json:"id,omitempty"` + // Name is the identifier of the function to be called. Name string `json:"name"` @@ -95,6 +98,9 @@ type FunctionCall struct { // FunctionResponse represents the result of a tool execution. // This is sent back to the model after a tool call has been processed. type FunctionResponse struct { + // ID is the identifier of the function to be called. + ID string `json:"id,omitempty"` + // Name is the identifier of the function that was called. Name string `json:"name"` diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index 00893c14..9857ffc2 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -89,10 +89,11 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" { functionName := contentResult.Get("name").String() functionArgs := contentResult.Get("input").String() + 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{Name: functionName, Args: args}, + FunctionCall: &client.FunctionCall{ID: functionID, Name: functionName, Args: args}, ThoughtSignature: geminiCLIClaudeThoughtSignature, }) } @@ -105,7 +106,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-") } responseData := contentResult.Get("content").Raw - functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}} + functionResponse := client.FunctionResponse{ID: toolCallID, Name: funcName, Response: map[string]interface{}{"result": responseData}} clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse}) } } diff --git a/internal/translator/antigravity/claude/antigravity_claude_response.go b/internal/translator/antigravity/claude/antigravity_claude_response.go index 6c699e1a..dd27a366 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_response.go +++ b/internal/translator/antigravity/claude/antigravity_claude_response.go @@ -141,35 +141,38 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq params.ResponseType = 2 // Set state to thinking } } else { - // Process regular text content (user-visible output) - // Continue existing text block if already in content state - if params.ResponseType == 1 { - 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) - } else { - // Transition from another state to text content - // First, close any existing content block - if params.ResponseType != 0 { - 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" + finishReasonResult := gjson.GetBytes(rawJSON, "response.candidates.0.finishReason") + if partTextResult.String() != "" || !finishReasonResult.Exists() { + // Process regular text content (user-visible output) + // Continue existing text block if already in content state + if params.ResponseType == 1 { + 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) + } else { + // Transition from another state to text content + // First, close any existing content block + if params.ResponseType != 0 { + 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" + } + output = output + "event: content_block_stop\n" + output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex) + output = output + "\n\n\n" + params.ResponseIndex++ } - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex) - 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 + // 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 + } } } } else if functionCallResult.Exists() { diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go index 2c1671f5..39fcbf50 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go @@ -251,6 +251,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ fid := tc.Get("id").String() fname := tc.Get("function.name").String() fargs := tc.Get("function.arguments").String() + node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature) @@ -266,6 +267,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ pp := 0 for _, fid := range fIDs { if name, ok := tcID2Name[fid]; ok { + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid) toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) resp := toolResponses[fid] if resp == "" { diff --git a/internal/translator/codex/gemini/codex_gemini_response.go b/internal/translator/codex/gemini/codex_gemini_response.go index 23a816ce..098e6228 100644 --- a/internal/translator/codex/gemini/codex_gemini_response.go +++ b/internal/translator/codex/gemini/codex_gemini_response.go @@ -327,7 +327,7 @@ func buildReverseMapFromGeminiOriginal(original []byte) map[string]string { func mustMarshalJSON(v interface{}) string { data, err := json.Marshal(v) if err != nil { - panic(err) + return "" } return string(data) } diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index 22ce913e..4ea75c18 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -249,6 +249,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte functionCall := `{"functionCall":{"name":"","args":{}}}` functionCall, _ = sjson.Set(functionCall, "functionCall.name", name) functionCall, _ = sjson.Set(functionCall, "thoughtSignature", geminiResponsesThoughtSignature) + functionCall, _ = sjson.Set(functionCall, "functionCall.id", item.Get("call_id").String()) // Parse arguments JSON string and set as args object if arguments != "" { @@ -285,6 +286,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte } functionResponse, _ = sjson.Set(functionResponse, "functionResponse.name", functionName) + functionResponse, _ = sjson.Set(functionResponse, "functionResponse.id", callID) // Set the raw JSON output directly (preserves string encoding) if outputRaw != "" && outputRaw != "null" {