sshinmen Claude commited on
Commit
bd5f2ec
·
1 Parent(s): 2e6b65c

Update gemini response translator

Browse files

Co-Authored-By: Claude <noreply@anthropic.com>

internal/translator/gemini/openai/chat-completions/gemini_openai_response.go CHANGED
@@ -140,31 +140,12 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
140
  hasFunctionCall := false
141
 
142
  // Handle Grounding (Google Search)
143
- // If we have search entry point (HTML) or web search queries, we should display them.
144
- // Ideally, we append this to the content so the user sees it.
145
  if groundingMetadata.Exists() {
146
  var groundingText strings.Builder
147
-
148
- // 1. Search Entry Point (rendered HTML usually)
149
  if rendered := groundingMetadata.Get("searchEntryPoint.renderedContent"); rendered.Exists() && rendered.String() != "" {
150
  groundingText.WriteString("\n\n" + rendered.String())
151
  }
152
-
153
- // 2. Web Search Queries (if available) - optional, maybe verbose
154
- // queries := groundingMetadata.Get("webSearchQueries")
155
-
156
- // Append to content if we have grounding info
157
  if groundingText.Len() > 0 {
158
- // We need to find the text part to append to, or create one.
159
- // If streaming, this might come in a separate chunk or the last chunk.
160
- // For simplicity in streaming, we can emit a separate content chunk for grounding.
161
-
162
- // However, we are inside a loop over candidates.
163
- // Let's modify the template to include this in the content delta if possible.
164
- // Or just append to the first text part found?
165
- // Or emit a new delta?
166
-
167
- // Safest: Emit a dedicated text delta for the grounding info.
168
  groundingTemplate := template // clone
169
  groundingTemplate, _ = sjson.Set(groundingTemplate, "choices.0.delta.content", groundingText.String())
170
  groundingTemplate, _ = sjson.Set(groundingTemplate, "choices.0.delta.role", "assistant")
@@ -184,6 +165,18 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
184
  if !inlineDataResult.Exists() {
185
  inlineDataResult = partResult.Get("inline_data")
186
  }
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
  if partTextResult.Exists() {
189
  text := partTextResult.String()
@@ -224,7 +217,6 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
224
  hasFunctionCall = true
225
  toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls")
226
 
227
- // Retrieve the function index for this specific candidate.
228
  functionCallIndex := p.FunctionIndex[candidateIndex]
229
  p.FunctionIndex[candidateIndex]++
230
 
@@ -254,6 +246,209 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
254
  template, _ = sjson.Set(template, "choices.0.delta.content", displayText)
255
  template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  } else if inlineDataResult.Exists() {
258
  data := inlineDataResult.Get("data").String()
259
  if data != "" {
 
140
  hasFunctionCall := false
141
 
142
  // Handle Grounding (Google Search)
 
 
143
  if groundingMetadata.Exists() {
144
  var groundingText strings.Builder
 
 
145
  if rendered := groundingMetadata.Get("searchEntryPoint.renderedContent"); rendered.Exists() && rendered.String() != "" {
146
  groundingText.WriteString("\n\n" + rendered.String())
147
  }
 
 
 
 
 
148
  if groundingText.Len() > 0 {
 
 
 
 
 
 
 
 
 
 
149
  groundingTemplate := template // clone
150
  groundingTemplate, _ = sjson.Set(groundingTemplate, "choices.0.delta.content", groundingText.String())
151
  groundingTemplate, _ = sjson.Set(groundingTemplate, "choices.0.delta.role", "assistant")
 
165
  if !inlineDataResult.Exists() {
166
  inlineDataResult = partResult.Get("inline_data")
167
  }
168
+ thoughtSignatureResult := partResult.Get("thoughtSignature")
169
+ if !thoughtSignatureResult.Exists() {
170
+ thoughtSignatureResult = partResult.Get("thought_signature")
171
+ }
172
+
173
+ hasThoughtSignature := thoughtSignatureResult.Exists() && thoughtSignatureResult.String() != ""
174
+ hasContentPayload := partTextResult.Exists() || functionCallResult.Exists() || inlineDataResult.Exists() || executableCodeResult.Exists() || codeExecutionResult.Exists()
175
+
176
+ // Skip pure thoughtSignature parts but keep any actual payload in the same part.
177
+ if hasThoughtSignature && !hasContentPayload {
178
+ continue
179
+ }
180
 
181
  if partTextResult.Exists() {
182
  text := partTextResult.String()
 
217
  hasFunctionCall = true
218
  toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls")
219
 
 
220
  functionCallIndex := p.FunctionIndex[candidateIndex]
221
  p.FunctionIndex[candidateIndex]++
222
 
 
246
  template, _ = sjson.Set(template, "choices.0.delta.content", displayText)
247
  template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
248
 
249
+ } else if inlineDataResult.Exists() {
250
+ data := inlineDataResult.Get("data").String()
251
+ if data != "" {
252
+ mimeType := inlineDataResult.Get("mimeType").String()
253
+ if mimeType == "" {
254
+ mimeType = inlineDataResult.Get("mime_type").String()
255
+ }
256
+ if mimeType == "" {
257
+ mimeType = "image/png"
258
+ }
259
+ imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
260
+ imagesResult := gjson.Get(template, "choices.0.delta.images")
261
+ if !imagesResult.Exists() || !imagesResult.IsArray() {
262
+ template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
263
+ }
264
+ imageIndex := len(gjson.Get(template, "choices.0.delta.images").Array())
265
+ imagePayload := `{"type":"image_url","image_url":{"url":""}}`
266
+ imagePayload, _ = sjson.Set(imagePayload, "index", imageIndex)
267
+ imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
268
+ template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
269
+ template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ if hasFunctionCall {
276
+ template, _ = sjson.Set(template, "choices.0.finish_reason", "tool_calls")
277
+ template, _ = sjson.Set(template, "choices.0.native_finish_reason", "tool_calls")
278
+ }
279
+
280
+ responseStrings = append(responseStrings, template)
281
+ return true
282
+ })
283
+ } else {
284
+ // If there are no candidates (e.g., a pure usageMetadata chunk), return the usage chunk if present.
285
+ if gjson.GetBytes(rawJSON, "usageMetadata").Exists() && len(responseStrings) == 0 {
286
+ responseStrings = append(responseStrings, baseTemplate)
287
+ }
288
+ }
289
+
290
+ return responseStrings
291
+ }
292
+
293
+ // ConvertGeminiResponseToOpenAINonStream converts a non-streaming Gemini response to a non-streaming OpenAI response.
294
+ // This function processes the complete Gemini response and transforms it into a single OpenAI-compatible
295
+ // JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all
296
+ // the information into a single response that matches the OpenAI API format.
297
+ //
298
+ // Parameters:
299
+ // - ctx: The context for the request, used for cancellation and timeout handling
300
+ // - modelName: The name of the model being used for the response (unused in current implementation)
301
+ // - rawJSON: The raw JSON response from the Gemini API
302
+ // - param: A pointer to a parameter object for the conversion (unused in current implementation)
303
+ //
304
+ // Returns:
305
+ // - string: An OpenAI-compatible JSON response containing all message content and metadata
306
+ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
307
+ var unixTimestamp int64
308
+ // Initialize template with an empty choices array to support multiple candidates.
309
+ template := `{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[]}`
310
+
311
+ if modelVersionResult := gjson.GetBytes(rawJSON, "modelVersion"); modelVersionResult.Exists() {
312
+ template, _ = sjson.Set(template, "model", modelVersionResult.String())
313
+ }
314
+
315
+ if createTimeResult := gjson.GetBytes(rawJSON, "createTime"); createTimeResult.Exists() {
316
+ t, err := time.Parse(time.RFC3339Nano, createTimeResult.String())
317
+ if err == nil {
318
+ unixTimestamp = t.Unix()
319
+ }
320
+ template, _ = sjson.Set(template, "created", unixTimestamp)
321
+ } else {
322
+ template, _ = sjson.Set(template, "created", unixTimestamp)
323
+ }
324
+
325
+ if responseIDResult := gjson.GetBytes(rawJSON, "responseId"); responseIDResult.Exists() {
326
+ template, _ = sjson.Set(template, "id", responseIDResult.String())
327
+ }
328
+
329
+ if usageResult := gjson.GetBytes(rawJSON, "usageMetadata"); usageResult.Exists() {
330
+ if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() {
331
+ template, _ = sjson.Set(template, "usage.completion_tokens", candidatesTokenCountResult.Int())
332
+ }
333
+ if totalTokenCountResult := usageResult.Get("totalTokenCount"); totalTokenCountResult.Exists() {
334
+ template, _ = sjson.Set(template, "usage.total_tokens", totalTokenCountResult.Int())
335
+ }
336
+ promptTokenCount := usageResult.Get("promptTokenCount").Int()
337
+ thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int()
338
+ cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int()
339
+ template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCount+thoughtsTokenCount)
340
+ if thoughtsTokenCount > 0 {
341
+ template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCount)
342
+ }
343
+ // Include cached token count if present (indicates prompt caching is working)
344
+ if cachedTokenCount > 0 {
345
+ var err error
346
+ template, err = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cachedTokenCount)
347
+ if err != nil {
348
+ log.Warnf("gemini openai response: failed to set cached_tokens in non-streaming: %v", err)
349
+ }
350
+ }
351
+ }
352
+
353
+ // Process the main content part of the response for all candidates.
354
+ candidates := gjson.GetBytes(rawJSON, "candidates")
355
+ if candidates.IsArray() {
356
+ candidates.ForEach(func(_, candidate gjson.Result) bool {
357
+ // Construct a single Choice object.
358
+ choiceTemplate := `{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}`
359
+
360
+ // Set the index for this choice.
361
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "index", candidate.Get("index").Int())
362
+
363
+ // Set finish reason.
364
+ if finishReasonResult := candidate.Get("finishReason"); finishReasonResult.Exists() {
365
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "finish_reason", strings.ToLower(finishReasonResult.String()))
366
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "native_finish_reason", strings.ToLower(finishReasonResult.String()))
367
+ }
368
+
369
+ // Handle Grounding (Google Search)
370
+ groundingMetadata := candidate.Get("groundingMetadata")
371
+ if groundingMetadata.Exists() {
372
+ var groundingText strings.Builder
373
+ if rendered := groundingMetadata.Get("searchEntryPoint.renderedContent"); rendered.Exists() && rendered.String() != "" {
374
+ groundingText.WriteString("\n\n" + rendered.String())
375
+ }
376
+ if groundingText.Len() > 0 {
377
+ oldVal := gjson.Get(choiceTemplate, "message.content").String()
378
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "message.content", oldVal+groundingText.String())
379
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "message.role", "assistant")
380
+ }
381
+ }
382
+
383
+ partsResult := candidate.Get("content.parts")
384
+ hasFunctionCall := false
385
+ if partsResult.IsArray() {
386
+ partsResults := partsResult.Array()
387
+ for i := 0; i < len(partsResults); i++ {
388
+ partResult := partsResults[i]
389
+ partTextResult := partResult.Get("text")
390
+ functionCallResult := partResult.Get("functionCall")
391
+ executableCodeResult := partResult.Get("executableCode")
392
+ codeExecutionResult := partResult.Get("codeExecutionResult")
393
+ inlineDataResult := partResult.Get("inlineData")
394
+ if !inlineDataResult.Exists() {
395
+ inlineDataResult = partResult.Get("inline_data")
396
+ }
397
+
398
+ if partTextResult.Exists() {
399
+ // Append text content, distinguishing between regular content and reasoning.
400
+ if partResult.Get("thought").Bool() {
401
+ oldVal := gjson.Get(choiceTemplate, "message.reasoning_content").String()
402
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "message.reasoning_content", oldVal+partTextResult.String())
403
+ } else {
404
+ oldVal := gjson.Get(choiceTemplate, "message.content").String()
405
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "message.content", oldVal+partTextResult.String())
406
+ }
407
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "message.role", "assistant")
408
+ } else if functionCallResult.Exists() {
409
+ // Append function call content to the tool_calls array.
410
+ hasFunctionCall = true
411
+ toolCallsResult := gjson.Get(choiceTemplate, "message.tool_calls")
412
+ if !toolCallsResult.Exists() || !toolCallsResult.IsArray() {
413
+ choiceTemplate, _ = sjson.SetRaw(choiceTemplate, "message.tool_calls", `[]`)
414
+ }
415
+ functionCallItemTemplate := `{"id": "","type": "function","function": {"name": "","arguments": ""}}`
416
+ fcName := functionCallResult.Get("name").String()
417
+ functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
418
+ functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.name", fcName)
419
+ if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
420
+ functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", fcArgsResult.Raw)
421
+ }
422
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "message.role", "assistant")
423
+ choiceTemplate, _ = sjson.SetRaw(choiceTemplate, "message.tool_calls.-1", functionCallItemTemplate)
424
+ } else if executableCodeResult.Exists() {
425
+ // Handle Gemini Code Execution (executableCode)
426
+ hasFunctionCall = true
427
+ toolCallsResult := gjson.Get(choiceTemplate, "message.tool_calls")
428
+ if !toolCallsResult.Exists() || !toolCallsResult.IsArray() {
429
+ choiceTemplate, _ = sjson.SetRaw(choiceTemplate, "message.tool_calls", `[]`)
430
+ }
431
+
432
+ code := executableCodeResult.Get("code").String()
433
+ codeArgs := fmt.Sprintf(`{"code": %q}`, code)
434
+
435
+ functionCallItemTemplate := `{"id": "","type": "function","function": {"name": "python","arguments": ""}}`
436
+ functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", fmt.Sprintf("call_code_%d_%d", time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
437
+ functionCallItemTemplate, _ = sjson.SetRaw(functionCallItemTemplate, "function.arguments", codeArgs)
438
+
439
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "message.role", "assistant")
440
+ choiceTemplate, _ = sjson.SetRaw(choiceTemplate, "message.tool_calls.-1", functionCallItemTemplate)
441
+
442
+ } else if codeExecutionResult.Exists() {
443
+ // Handle Gemini Code Execution Result
444
+ output := codeExecutionResult.Get("output").String()
445
+ outcome := codeExecutionResult.Get("outcome").String()
446
+ displayText := fmt.Sprintf("\n\n> **Code Execution (%s):**\n```\n%s\n```\n", outcome, output)
447
+
448
+ oldVal := gjson.Get(choiceTemplate, "message.content").String()
449
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "message.content", oldVal+displayText)
450
+ choiceTemplate, _ = sjson.Set(choiceTemplate, "message.role", "assistant")
451
+
452
  } else if inlineDataResult.Exists() {
453
  data := inlineDataResult.Get("data").String()
454
  if data != "" {