refactor(translator): simplify response logic and enhance thinking compatibility handling

- Consolidated thinking content block handling for improved clarity and efficiency.
- Unified `generationConfig` handling, including normalization of `thinkingConfig` settings.
- Added `applyOpenAIThinkingCompatibilityToAntigravity` for consistent reasoning and thought configuration compatibility.

Closes: #4067
This commit is contained in:
Luis Pater
2026-07-08 04:37:16 +08:00
parent dea4787970
commit 14b139661d
5 changed files with 325 additions and 68 deletions
@@ -212,83 +212,51 @@ func ConvertAntigravityResponseToClaude(ctx context.Context, _ string, originalR
// Handle text content (both regular content and thinking)
if partTextResult.Exists() {
// Process thinking content (internal reasoning)
if partResult.Get("thought").Bool() || hasThoughtSignature {
if hasThoughtSignature {
// log.Debug("Branch: signature_delta")
// Flush co-located text before emitting the signature
if partText := partTextResult.String(); partText != "" {
if params.ResponseType != 2 {
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.ResponseType = 2
params.CurrentThinkingText.Reset()
}
partText := partTextResult.String()
if partResult.Get("thought").Bool() {
if partText != "" {
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))
}
appendThinkingSignature(thoughtSignatureResult.String())
} else if params.ResponseType == 2 { // Continue existing thinking block if already in thinking state
params.CurrentThinkingText.WriteString(partTextResult.String())
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex)), "delta.thinking", partTextResult.String())
appendEvent("content_block_delta", string(data))
params.HasContent = true
} else {
// Transition from another state to thinking
// 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"
}
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex))
params.ResponseIndex++
}
// Start a new thinking content block
appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, params.ResponseIndex))
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex)), "delta.thinking", partTextResult.String())
appendEvent("content_block_delta", string(data))
params.ResponseType = 2 // Set state to thinking
params.HasContent = true
// Start accumulating thinking text for signature caching
params.CurrentThinkingText.Reset()
params.CurrentThinkingText.WriteString(partTextResult.String())
}
} else {
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 {
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex)), "delta.text", partTextResult.String())
appendEvent("content_block_delta", string(data))
params.HasContent = true
} 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"
}
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex))
params.ResponseIndex++
}
if partTextResult.String() != "" {
// Start a new text content block
appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, params.ResponseIndex))
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())
}
} else {
if hasThoughtSignature {
appendThinkingSignature(thoughtSignatureResult.String())
}
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", partTextResult.String())
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 // Set state to content
params.ResponseType = 1
params.HasContent = true
}
}
@@ -556,8 +524,8 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
sig = part.Get("thought_signature")
}
hasThoughtSignature := sig.Exists() && sig.String() != "" && !part.Get("functionCall").Exists()
isThought := part.Get("thought").Bool() || hasThoughtSignature
if hasThoughtSignature {
isThought := part.Get("thought").Bool()
if hasThoughtSignature && (isThought || thinkingBuilder.Len() > 0) {
thinkingSignature = sig.String()
}
@@ -755,3 +755,105 @@ func TestConvertAntigravityResponseToClaudeNonStream_SignatureOnlyPartWithoutTho
t.Fatalf("expected signature %q, got %q: %s", validSignature, got, output)
}
}
func TestConvertAntigravityResponseToClaudeNonStream_TextWithThoughtSignatureStaysText(t *testing.T) {
previousCache := cache.SignatureCacheEnabled()
cache.SetSignatureCacheEnabled(false)
defer cache.SetSignatureCacheEnabled(previousCache)
requestJSON := []byte(`{"model":"gemini-3.1-pro-low"}`)
translatedRequestJSON := []byte(`{"model":"gemini-3.1-pro-low"}`)
responseJSON := []byte(`{
"response": {
"candidates": [{
"content": {
"parts": [
{"text": "I need to multiply 17 by 24.", "thought": true},
{"text": "408", "thoughtSignature": "sig-final-answer"}
]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": 16,
"candidatesTokenCount": 3,
"thoughtsTokenCount": 42,
"totalTokenCount": 61
},
"modelVersion": "gemini-3.1-pro-low",
"responseId": "resp-text-sig"
}
}`)
output := ConvertAntigravityResponseToClaudeNonStream(context.Background(), "gemini-3.1-pro-low", requestJSON, translatedRequestJSON, responseJSON, nil)
if got := gjson.GetBytes(output, "content.#").Int(); got != 2 {
t.Fatalf("content block count = %d, want 2. Output: %s", got, output)
}
if got := gjson.GetBytes(output, "content.0.type").String(); got != "thinking" {
t.Fatalf("content.0.type = %q, want thinking. Output: %s", got, output)
}
if got := gjson.GetBytes(output, "content.0.thinking").String(); got != "I need to multiply 17 by 24." {
t.Fatalf("thinking = %q, want thought text. Output: %s", got, output)
}
if got := gjson.GetBytes(output, "content.0.signature").String(); got != "sig-final-answer" {
t.Fatalf("signature = %q, want sig-final-answer. Output: %s", got, output)
}
if got := gjson.GetBytes(output, "content.1.type").String(); got != "text" {
t.Fatalf("content.1.type = %q, want text. Output: %s", got, output)
}
if got := gjson.GetBytes(output, "content.1.text").String(); got != "408" {
t.Fatalf("text = %q, want final answer. Output: %s", got, output)
}
}
func TestConvertAntigravityResponseToClaudeStream_TextWithThoughtSignatureStaysText(t *testing.T) {
previousCache := cache.SignatureCacheEnabled()
cache.SetSignatureCacheEnabled(false)
defer cache.SetSignatureCacheEnabled(previousCache)
requestJSON := []byte(`{"model":"gemini-3.1-pro-low"}`)
translatedRequestJSON := []byte(`{"model":"gemini-3.1-pro-low"}`)
thoughtChunk := []byte(`{
"response": {
"candidates": [{"content": {"parts": [{"text": "I need to multiply 17 by 24.", "thought": true}]}}],
"modelVersion": "gemini-3.1-pro-low",
"responseId": "resp-text-sig"
}
}`)
textChunk := []byte(`{
"response": {
"candidates": [{"content": {"parts": [{"text": "408", "thoughtSignature": "sig-final-answer"}]}}],
"modelVersion": "gemini-3.1-pro-low",
"responseId": "resp-text-sig"
}
}`)
finishChunk := []byte(`{
"response": {
"candidates": [{"finishReason": "STOP"}],
"usageMetadata": {"promptTokenCount": 16, "candidatesTokenCount": 3, "thoughtsTokenCount": 42, "totalTokenCount": 61},
"modelVersion": "gemini-3.1-pro-low",
"responseId": "resp-text-sig"
}
}`)
var param any
ctx := context.Background()
output := bytes.Join(ConvertAntigravityResponseToClaude(ctx, "gemini-3.1-pro-low", requestJSON, translatedRequestJSON, thoughtChunk, &param), nil)
output = append(output, bytes.Join(ConvertAntigravityResponseToClaude(ctx, "gemini-3.1-pro-low", requestJSON, translatedRequestJSON, textChunk, &param), nil)...)
output = append(output, bytes.Join(ConvertAntigravityResponseToClaude(ctx, "gemini-3.1-pro-low", requestJSON, translatedRequestJSON, finishChunk, &param), nil)...)
output = append(output, bytes.Join(ConvertAntigravityResponseToClaude(ctx, "gemini-3.1-pro-low", requestJSON, translatedRequestJSON, []byte("[DONE]"), &param), nil)...)
outputText := string(output)
if !strings.Contains(outputText, `"delta":{"type":"signature_delta","signature":"sig-final-answer"}`) {
t.Fatalf("expected signature delta for thinking block: %s", outputText)
}
if !strings.Contains(outputText, `"content_block":{"type":"text","text":""}`) {
t.Fatalf("expected text content block after thinking: %s", outputText)
}
if !strings.Contains(outputText, `"delta":{"type":"text_delta","text":"408"}`) {
t.Fatalf("expected final answer as text delta: %s", outputText)
}
if strings.Contains(outputText, `"delta":{"type":"thinking_delta","thinking":"408"}`) {
t.Fatalf("final answer must not be emitted as thinking delta: %s", outputText)
}
}
@@ -37,6 +37,8 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
// Let user-provided generationConfig pass through
if genConfig := gjson.GetBytes(rawJSON, "generationConfig"); genConfig.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig", []byte(genConfig.Raw))
} else if genConfig := gjson.GetBytes(rawJSON, "generation_config"); genConfig.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig", []byte(genConfig.Raw))
}
// Apply thinking configuration: convert OpenAI reasoning_effort to Antigravity thinkingConfig.
@@ -55,6 +57,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
}
}
}
out = applyOpenAIThinkingCompatibilityToAntigravity(out, rawJSON, modelName)
// Temperature/top_p/top_k/max_tokens
if tr := gjson.GetBytes(rawJSON, "temperature"); tr.Exists() && tr.Type == gjson.Number {
@@ -451,5 +454,83 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
return common.AttachDefaultSafetySettings(out, "request.safetySettings")
}
func applyOpenAIThinkingCompatibilityToAntigravity(out []byte, rawJSON []byte, modelName string) []byte {
out = normalizeAntigravityOpenAIThinkingConfig(out)
for _, path := range []string{
"thinking.includeThoughts",
"thinking.include_thoughts",
"reasoning.includeThoughts",
"reasoning.include_thoughts",
} {
if value := gjson.GetBytes(rawJSON, path); value.Exists() {
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", value.Bool())
}
}
if exclude := gjson.GetBytes(rawJSON, "reasoning.exclude"); exclude.Exists() {
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", !exclude.Bool())
}
if !gjson.GetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts").Exists() && antigravityOpenAIDefaultIncludeThoughts(modelName) {
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
}
return normalizeAntigravityOpenAIThinkingConfig(out)
}
func normalizeAntigravityOpenAIThinkingConfig(out []byte) []byte {
for _, prefix := range []string{
"request.generationConfig.thinking_config",
"request.generationConfig.thinkingConfig",
} {
if includeThoughts := gjson.GetBytes(out, prefix+".includeThoughts"); includeThoughts.Exists() {
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts.Bool())
}
if includeThoughts := gjson.GetBytes(out, prefix+".include_thoughts"); includeThoughts.Exists() {
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts.Bool())
}
if thinkingLevel := gjson.GetBytes(out, prefix+".thinkingLevel"); thinkingLevel.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel", []byte(thinkingLevel.Raw))
}
if thinkingLevel := gjson.GetBytes(out, prefix+".thinking_level"); thinkingLevel.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel", []byte(thinkingLevel.Raw))
}
if thinkingBudget := gjson.GetBytes(out, prefix+".thinkingBudget"); thinkingBudget.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig.thinkingConfig.thinkingBudget", []byte(thinkingBudget.Raw))
}
if thinkingBudget := gjson.GetBytes(out, prefix+".thinking_budget"); thinkingBudget.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig.thinkingConfig.thinkingBudget", []byte(thinkingBudget.Raw))
}
}
for _, path := range []string{
"request.generationConfig.includeThoughts",
"request.generationConfig.include_thoughts",
} {
if includeThoughts := gjson.GetBytes(out, path); includeThoughts.Exists() {
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts.Bool())
}
}
for _, path := range []string{
"request.generationConfig.thinking_config",
"request.generationConfig.thinkingConfig.include_thoughts",
"request.generationConfig.thinkingConfig.thinking_level",
"request.generationConfig.thinkingConfig.thinking_budget",
"request.generationConfig.includeThoughts",
"request.generationConfig.include_thoughts",
} {
out, _ = sjson.DeleteBytes(out, path)
}
return out
}
func antigravityOpenAIDefaultIncludeThoughts(modelName string) bool {
modelName = strings.ToLower(modelName)
return strings.Contains(modelName, "gemini-3")
}
// itoa converts int to string without strconv import for few usages.
func itoa(i int) string { return fmt.Sprintf("%d", i) }
@@ -54,3 +54,72 @@ func TestConvertOpenAIRequestToAntigravitySkipsEmptyTextPartsWithoutNulls(t *tes
t.Fatalf("functionCall missing. Output: %s", result)
}
}
func TestConvertOpenAIRequestToAntigravityThinkingAliases(t *testing.T) {
tests := []struct {
name string
body string
want bool
}{
{
name: "Default Gemini include thoughts",
body: `{
"model":"gemini-3.1-pro-low",
"messages":[{"role":"user","content":"hi"}]
}`,
want: true,
},
{
name: "GenerationConfig snake include thoughts",
body: `{
"model":"gemini-3.1-pro-low",
"messages":[{"role":"user","content":"hi"}],
"generationConfig":{"thinkingConfig":{"include_thoughts":true}}
}`,
want: true,
},
{
name: "Top-level thinking include thoughts",
body: `{
"model":"gemini-3.1-pro-low",
"messages":[{"role":"user","content":"hi"}],
"thinking":{"include_thoughts":true}
}`,
want: true,
},
{
name: "Reasoning exclude false includes thoughts",
body: `{
"model":"gemini-3.1-pro-low",
"messages":[{"role":"user","content":"hi"}],
"reasoning":{"exclude":false}
}`,
want: true,
},
{
name: "Reasoning exclude true hides thoughts",
body: `{
"model":"gemini-3.1-pro-low",
"messages":[{"role":"user","content":"hi"}],
"reasoning":{"exclude":true}
}`,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ConvertOpenAIRequestToAntigravity("gemini-3.1-pro-low", []byte(tt.body), false)
includeThoughts := gjson.GetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts")
if !includeThoughts.Exists() {
t.Fatalf("includeThoughts missing. Output: %s", result)
}
if got := includeThoughts.Bool(); got != tt.want {
t.Fatalf("includeThoughts = %v, want %v. Output: %s", got, tt.want, result)
}
if snake := gjson.GetBytes(result, "request.generationConfig.thinkingConfig.include_thoughts"); snake.Exists() {
t.Fatalf("include_thoughts should be normalized away. Output: %s", result)
}
})
}
}
@@ -126,3 +126,40 @@ func TestNoFinishReasonOnIntermediateChunks(t *testing.T) {
t.Errorf("Expected no finish_reason on intermediate chunk, got: %v", fr2)
}
}
func TestConvertAntigravityResponseToOpenAINonStreamIncludesReasoningContent(t *testing.T) {
ctx := context.Background()
responseJSON := []byte(`{
"response": {
"candidates": [{
"index": 0,
"content": {
"parts": [
{"text": "I need to multiply 17 by 24.", "thought": true},
{"text": "408", "thoughtSignature": "sig-final-answer"}
]
},
"finishReason": "STOP"
}],
"usageMetadata": {
"promptTokenCount": 16,
"candidatesTokenCount": 3,
"thoughtsTokenCount": 42,
"totalTokenCount": 61
},
"modelVersion": "gemini-3.1-pro-low",
"responseId": "resp-reasoning"
}
}`)
output := ConvertAntigravityResponseToOpenAINonStream(ctx, "gemini-3.1-pro-low", nil, nil, responseJSON, nil)
if got := gjson.GetBytes(output, "choices.0.message.reasoning_content").String(); got != "I need to multiply 17 by 24." {
t.Fatalf("reasoning_content = %q, want thought text. Output: %s", got, output)
}
if got := gjson.GetBytes(output, "choices.0.message.content").String(); got != "408" {
t.Fatalf("content = %q, want final answer. Output: %s", got, output)
}
if got := gjson.GetBytes(output, "usage.completion_tokens_details.reasoning_tokens").Int(); got != 42 {
t.Fatalf("reasoning_tokens = %d, want 42. Output: %s", got, output)
}
}