History

Merge pull request #479 from router-for-me/claude

fix(claude): prevent final events when no content streamed

Luis Pater committed 11 months ago

Commit 1da03bf2 parents 423ce97 a8951493 files · +83 −42Browse files

Unified · Split

3 files
  1. internal/translator/antigravity/claude/antigravity_claude_response.go +19 −4
  2. internal/translator/gemini-cli/claude/gemini-cli_claude_response.go +35 −22
  3. internal/translator/gemini/claude/gemini_claude_response.go +29 −16
internal/translator/antigravity/claude/antigravity_claude_response.go+19−4
@@ -35,6 +35,7 @@ type Params struct { · 34 unmodified lines
3535 TotalTokenCount int64 // Cached total token count from usage metadata
3636 HasSentFinalEvents bool // Indicates if final content/message events have been sent
3737 HasToolUse bool // Indicates if tool use was observed in the stream
38+ HasContent bool // Tracks whether any content (text, thinking, or tool use) has been output
3839 }
3940
4041 // toolUseIDCounter provides a process-wide unique counter for tool use identifiers.
@@ -69,11 +70,14 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq · 28 unmodified lines
6970
7071 if bytes.Equal(rawJSON, []byte("[DONE]")) {
7172 output := ""
72− appendFinalEvents(params, &output, true)
73−
74− return []string{
75− output + "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n",
73+ // Only send final events if we have actually output content
74+ if params.HasContent {
75+ appendFinalEvents(params, &output, true)
76+ return []string{
77+ output + "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n",
78+ }
7679 }
80+ return []string{}
7781 }
7882
7983 output := ""
@@ -119,10 +123,12 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq · 39 unmodified lines
119123 output = output + "event: content_block_delta\n"
120124 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex), "delta.signature", thoughtSignature.String())
121125 output = output + fmt.Sprintf("data: %s\n\n\n", data)
126+ params.HasContent = true
122127 } else if params.ResponseType == 2 { // Continue existing thinking block if already in thinking state
123128 output = output + "event: content_block_delta\n"
124129 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex), "delta.thinking", partTextResult.String())
125130 output = output + fmt.Sprintf("data: %s\n\n\n", data)
131+ params.HasContent = true
126132 } else {
127133 // Transition from another state to thinking
128134 // First, close any existing content block
@@ -146,6 +152,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq · 17 unmodified lines
146152 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex), "delta.thinking", partTextResult.String())
147153 output = output + fmt.Sprintf("data: %s\n\n\n", data)
148154 params.ResponseType = 2 // Set state to thinking
155+ params.HasContent = true
149156 }
150157 } else {
151158 finishReasonResult := gjson.GetBytes(rawJSON, "response.candidates.0.finishReason")
@@ -156,6 +163,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq · 4 unmodified lines
156163 output = output + "event: content_block_delta\n"
157164 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex), "delta.text", partTextResult.String())
158165 output = output + fmt.Sprintf("data: %s\n\n\n", data)
166+ params.HasContent = true
159167 } else {
160168 // Transition from another state to text content
161169 // First, close any existing content block
@@ -179,6 +187,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq · 17 unmodified lines
179187 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex), "delta.text", partTextResult.String())
180188 output = output + fmt.Sprintf("data: %s\n\n\n", data)
181189 params.ResponseType = 1 // Set state to content
190+ params.HasContent = true
182191 }
183192 }
184193 }
@@ -230,6 +239,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq · 45 unmodified lines
230239 output = output + fmt.Sprintf("data: %s\n\n\n", data)
231240 }
232241 params.ResponseType = 3
242+ params.HasContent = true
233243 }
234244 }
235245 }
@@ -269,6 +279,11 @@ func appendFinalEvents(params *Params, output *string, force bool) { · 33 unmodified lines
269279 return
270280 }
271281
282+ // Only send final events if we have actually output content
283+ if !params.HasContent {
284+ return
285+ }
286+
272287 if params.ResponseType != 0 {
273288 *output = *output + "event: content_block_stop\n"
274289 *output = *output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex)
internal/translator/gemini-cli/claude/gemini-cli_claude_response.go+35−22
@@ -26,6 +26,7 @@ type Params struct { · 25 unmodified lines
2626 HasFirstResponse bool // Indicates if the initial message_start event has been sent
2727 ResponseType int // Current response type: 0=none, 1=content, 2=thinking, 3=function
2828 ResponseIndex int // Index counter for content blocks in the streaming response
29+ HasContent bool // Tracks whether any content (text, thinking, or tool use) has been output
2930 }
3031
3132 // toolUseIDCounter provides a process-wide unique counter for tool use identifiers.
@@ -57,9 +58,13 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque · 25 unmodified lines
5758 }
5859
5960 if bytes.Equal(rawJSON, []byte("[DONE]")) {
60− return []string{
61− "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n",
61+ // Only send message_stop if we have actually output content
62+ if (*param).(*Params).HasContent {
63+ return []string{
64+ "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n",
65+ }
6266 }
67+ return []string{}
6368 }
6469
6570 // Track whether tools are being used in this response chunk
@@ -108,6 +113,7 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque · 42 unmodified lines
108113 output = output + "event: content_block_delta\n"
109114 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex), "delta.thinking", partTextResult.String())
110115 output = output + fmt.Sprintf("data: %s\n\n\n", data)
116+ (*param).(*Params).HasContent = true
111117 } else {
112118 // Transition from another state to thinking
113119 // First, close any existing content block
@@ -131,6 +137,7 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque · 17 unmodified lines
131137 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex), "delta.thinking", partTextResult.String())
132138 output = output + fmt.Sprintf("data: %s\n\n\n", data)
133139 (*param).(*Params).ResponseType = 2 // Set state to thinking
140+ (*param).(*Params).HasContent = true
134141 }
135142 } else {
136143 // Process regular text content (user-visible output)
@@ -139,6 +146,7 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque · 2 unmodified lines
139146 output = output + "event: content_block_delta\n"
140147 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex), "delta.text", partTextResult.String())
141148 output = output + fmt.Sprintf("data: %s\n\n\n", data)
149+ (*param).(*Params).HasContent = true
142150 } else {
143151 // Transition from another state to text content
144152 // First, close any existing content block
@@ -162,6 +170,7 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque · 17 unmodified lines
162170 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex), "delta.text", partTextResult.String())
163171 output = output + fmt.Sprintf("data: %s\n\n\n", data)
164172 (*param).(*Params).ResponseType = 1 // Set state to content
173+ (*param).(*Params).HasContent = true
165174 }
166175 }
167176 } else if functionCallResult.Exists() {
@@ -211,6 +220,7 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque · 43 unmodified lines
211220 output = output + fmt.Sprintf("data: %s\n\n\n", data)
212221 }
213222 (*param).(*Params).ResponseType = 3
223+ (*param).(*Params).HasContent = true
214224 }
215225 }
216226 }
@@ -219,28 +229,31 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque · 2 unmodified lines
219229 // Process usage metadata and finish reason when present in the response
220230 if usageResult.Exists() && bytes.Contains(rawJSON, []byte(`"finishReason"`)) {
221231 if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() {
222− // Close the final content block
223− output = output + "event: content_block_stop\n"
224− output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
225− output = output + "\n\n\n"
226−
227− // Send the final message delta with usage information and stop reason
228− output = output + "event: message_delta\n"
229− output = output + `data: `
230−
231− // Create the message delta template with appropriate stop reason
232− template := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
233− // Set tool_use stop reason if tools were used in this response
234− if usedTool {
235− template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
236− }
232+ // Only send final events if we have actually output content
233+ if (*param).(*Params).HasContent {
234+ // Close the final content block
235+ output = output + "event: content_block_stop\n"
236+ output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
237+ output = output + "\n\n\n"
238+
239+ // Send the final message delta with usage information and stop reason
240+ output = output + "event: message_delta\n"
241+ output = output + `data: `
242+
243+ // Create the message delta template with appropriate stop reason
244+ template := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
245+ // Set tool_use stop reason if tools were used in this response
246+ if usedTool {
247+ template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
248+ }
237249
238− // Include thinking tokens in output token count if present
239− thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int()
240− template, _ = sjson.Set(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount)
241− template, _ = sjson.Set(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int())
250+ // Include thinking tokens in output token count if present
251+ thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int()
252+ template, _ = sjson.Set(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount)
253+ template, _ = sjson.Set(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int())
242254
243− output = output + template + "\n\n\n"
255+ output = output + template + "\n\n\n"
256+ }
244257 }
245258 }
246259
internal/translator/gemini/claude/gemini_claude_response.go+29−16
@@ -25,6 +25,7 @@ type Params struct { · 24 unmodified lines
2525 HasFirstResponse bool
2626 ResponseType int
2727 ResponseIndex int
28+ HasContent bool // Tracks whether any content (text, thinking, or tool use) has been output
2829 }
2930
3031 // toolUseIDCounter provides a process-wide unique counter for tool use identifiers.
@@ -57,9 +58,13 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR · 26 unmodified lines
5758 }
5859
5960 if bytes.Equal(rawJSON, []byte("[DONE]")) {
60− return []string{
61− "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n",
61+ // Only send message_stop if we have actually output content
62+ if (*param).(*Params).HasContent {
63+ return []string{
64+ "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n",
65+ }
6266 }
67+ return []string{}
6368 }
6469
6570 // Track whether tools are being used in this response chunk
@@ -108,6 +113,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR · 42 unmodified lines
108113 output = output + "event: content_block_delta\n"
109114 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex), "delta.thinking", partTextResult.String())
110115 output = output + fmt.Sprintf("data: %s\n\n\n", data)
116+ (*param).(*Params).HasContent = true
111117 } else {
112118 // Transition from another state to thinking
113119 // First, close any existing content block
@@ -131,6 +137,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR · 17 unmodified lines
131137 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex), "delta.thinking", partTextResult.String())
132138 output = output + fmt.Sprintf("data: %s\n\n\n", data)
133139 (*param).(*Params).ResponseType = 2 // Set state to thinking
140+ (*param).(*Params).HasContent = true
134141 }
135142 } else {
136143 // Process regular text content (user-visible output)
@@ -139,6 +146,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR · 2 unmodified lines
139146 output = output + "event: content_block_delta\n"
140147 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex), "delta.text", partTextResult.String())
141148 output = output + fmt.Sprintf("data: %s\n\n\n", data)
149+ (*param).(*Params).HasContent = true
142150 } else {
143151 // Transition from another state to text content
144152 // First, close any existing content block
@@ -162,6 +170,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR · 17 unmodified lines
162170 data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex), "delta.text", partTextResult.String())
163171 output = output + fmt.Sprintf("data: %s\n\n\n", data)
164172 (*param).(*Params).ResponseType = 1 // Set state to content
173+ (*param).(*Params).HasContent = true
165174 }
166175 }
167176 } else if functionCallResult.Exists() {
@@ -211,6 +220,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR · 43 unmodified lines
211220 output = output + fmt.Sprintf("data: %s\n\n\n", data)
212221 }
213222 (*param).(*Params).ResponseType = 3
223+ (*param).(*Params).HasContent = true
214224 }
215225 }
216226 }
@@ -218,23 +228,26 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR · 1 unmodified line
218228 usageResult := gjson.GetBytes(rawJSON, "usageMetadata")
219229 if usageResult.Exists() && bytes.Contains(rawJSON, []byte(`"finishReason"`)) {
220230 if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() {
221− output = output + "event: content_block_stop\n"
222− output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
223− output = output + "\n\n\n"
231+ // Only send final events if we have actually output content
232+ if (*param).(*Params).HasContent {
233+ output = output + "event: content_block_stop\n"
234+ output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
235+ output = output + "\n\n\n"
236+
237+ output = output + "event: message_delta\n"
238+ output = output + `data: `
239+
240+ template := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
241+ if usedTool {
242+ template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
243+ }
224244
225− output = output + "event: message_delta\n"
226− output = output + `data: `
245+ thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int()
246+ template, _ = sjson.Set(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount)
247+ template, _ = sjson.Set(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int())
227248
228− template := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
229− if usedTool {
230− template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
249+ output = output + template + "\n\n\n"
231250 }
232−
233− thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int()
234− template, _ = sjson.Set(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount)
235− template, _ = sjson.Set(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int())
236−
237− output = output + template + "\n\n\n"
238251 }
239252 }
240253