mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 20:30:51 +08:00
252 lines
9.5 KiB
Go
252 lines
9.5 KiB
Go
// Package code provides response translation functionality for Gemini API.
|
|
// This package handles the conversion of Codex backend responses into Gemini-compatible
|
|
// JSON format, transforming streaming events into single-line JSON responses that include
|
|
// thinking content, regular text content, and function calls in the format expected by
|
|
// Gemini API clients.
|
|
package code
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
type ConvertCodexResponseToGeminiParams struct {
|
|
Model string
|
|
CreatedAt int64
|
|
ResponseID string
|
|
LastStorageOutput string
|
|
}
|
|
|
|
// ConvertCodexResponseToGemini converts Codex streaming response format to Gemini single-line JSON format.
|
|
// This function processes various Codex event types and transforms them into Gemini-compatible JSON responses.
|
|
// It handles thinking content, regular text content, and function calls, outputting single-line JSON
|
|
// that matches the Gemini API response format.
|
|
// The lastEventType parameter tracks the previous event type to handle consecutive function calls properly.
|
|
func ConvertCodexResponseToGemini(rawJSON []byte, param *ConvertCodexResponseToGeminiParams) []string {
|
|
rootResult := gjson.ParseBytes(rawJSON)
|
|
typeResult := rootResult.Get("type")
|
|
typeStr := typeResult.String()
|
|
|
|
// Base Gemini response template
|
|
template := `{"candidates":[{"content":{"role":"model","parts":[]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"gemini-2.5-pro","createTime":"2025-08-15T02:52:03.884209Z","responseId":"06CeaPH7NaCU48APvNXDyA4"}`
|
|
if param.LastStorageOutput != "" && typeStr == "response.output_item.done" {
|
|
template = param.LastStorageOutput
|
|
} else {
|
|
template, _ = sjson.Set(template, "modelVersion", param.Model)
|
|
createdAtResult := rootResult.Get("response.created_at")
|
|
if createdAtResult.Exists() {
|
|
param.CreatedAt = createdAtResult.Int()
|
|
template, _ = sjson.Set(template, "createTime", time.Unix(param.CreatedAt, 0).Format(time.RFC3339Nano))
|
|
}
|
|
template, _ = sjson.Set(template, "responseId", param.ResponseID)
|
|
}
|
|
|
|
// Handle function call completion
|
|
if typeStr == "response.output_item.done" {
|
|
itemResult := rootResult.Get("item")
|
|
itemType := itemResult.Get("type").String()
|
|
if itemType == "function_call" {
|
|
// Create function call part
|
|
functionCall := `{"functionCall":{"name":"","args":{}}}`
|
|
functionCall, _ = sjson.Set(functionCall, "functionCall.name", itemResult.Get("name").String())
|
|
|
|
// Parse and set arguments
|
|
argsStr := itemResult.Get("arguments").String()
|
|
if argsStr != "" {
|
|
argsResult := gjson.Parse(argsStr)
|
|
if argsResult.IsObject() {
|
|
functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsStr)
|
|
}
|
|
}
|
|
|
|
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", functionCall)
|
|
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
|
|
|
param.LastStorageOutput = template
|
|
|
|
// Use this return to storage message
|
|
return []string{}
|
|
}
|
|
}
|
|
|
|
if typeStr == "response.created" { // Handle response creation - set model and response ID
|
|
template, _ = sjson.Set(template, "modelVersion", rootResult.Get("response.model").String())
|
|
template, _ = sjson.Set(template, "responseId", rootResult.Get("response.id").String())
|
|
param.ResponseID = rootResult.Get("response.id").String()
|
|
} else if typeStr == "response.reasoning_summary_text.delta" { // Handle reasoning/thinking content delta
|
|
part := `{"thought":true,"text":""}`
|
|
part, _ = sjson.Set(part, "text", rootResult.Get("delta").String())
|
|
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
|
|
} else if typeStr == "response.output_text.delta" { // Handle regular text content delta
|
|
part := `{"text":""}`
|
|
part, _ = sjson.Set(part, "text", rootResult.Get("delta").String())
|
|
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
|
|
} else if typeStr == "response.completed" { // Handle response completion with usage metadata
|
|
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", rootResult.Get("response.usage.input_tokens").Int())
|
|
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", rootResult.Get("response.usage.output_tokens").Int())
|
|
totalTokens := rootResult.Get("response.usage.input_tokens").Int() + rootResult.Get("response.usage.output_tokens").Int()
|
|
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", totalTokens)
|
|
} else {
|
|
return []string{}
|
|
}
|
|
|
|
if param.LastStorageOutput != "" {
|
|
return []string{param.LastStorageOutput, template}
|
|
} else {
|
|
return []string{template}
|
|
}
|
|
|
|
}
|
|
|
|
// ConvertCodexResponseToGeminiNonStream converts a completed Codex response to Gemini non-streaming format.
|
|
// This function processes the final response.completed event and transforms it into a complete
|
|
// Gemini-compatible JSON response that includes all content parts, function calls, and usage metadata.
|
|
func ConvertCodexResponseToGeminiNonStream(rawJSON []byte, model string) string {
|
|
rootResult := gjson.ParseBytes(rawJSON)
|
|
|
|
// Verify this is a response.completed event
|
|
if rootResult.Get("type").String() != "response.completed" {
|
|
return ""
|
|
}
|
|
|
|
// Base Gemini response template for non-streaming
|
|
template := `{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`
|
|
|
|
// Set model version
|
|
template, _ = sjson.Set(template, "modelVersion", model)
|
|
|
|
// Set response metadata from the completed response
|
|
responseData := rootResult.Get("response")
|
|
if responseData.Exists() {
|
|
// Set response ID
|
|
if responseId := responseData.Get("id"); responseId.Exists() {
|
|
template, _ = sjson.Set(template, "responseId", responseId.String())
|
|
}
|
|
|
|
// Set creation time
|
|
if createdAt := responseData.Get("created_at"); createdAt.Exists() {
|
|
template, _ = sjson.Set(template, "createTime", time.Unix(createdAt.Int(), 0).Format(time.RFC3339Nano))
|
|
}
|
|
|
|
// Set usage metadata
|
|
if usage := responseData.Get("usage"); usage.Exists() {
|
|
inputTokens := usage.Get("input_tokens").Int()
|
|
outputTokens := usage.Get("output_tokens").Int()
|
|
totalTokens := inputTokens + outputTokens
|
|
|
|
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", inputTokens)
|
|
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", outputTokens)
|
|
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", totalTokens)
|
|
}
|
|
|
|
// Process output content to build parts array
|
|
var parts []interface{}
|
|
hasToolCall := false
|
|
var pendingFunctionCalls []interface{}
|
|
|
|
flushPendingFunctionCalls := func() {
|
|
if len(pendingFunctionCalls) > 0 {
|
|
// Add all pending function calls as individual parts
|
|
// This maintains the original Gemini API format while ensuring consecutive calls are grouped together
|
|
for _, fc := range pendingFunctionCalls {
|
|
parts = append(parts, fc)
|
|
}
|
|
pendingFunctionCalls = nil
|
|
}
|
|
}
|
|
|
|
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
|
output.ForEach(func(key, value gjson.Result) bool {
|
|
itemType := value.Get("type").String()
|
|
|
|
switch itemType {
|
|
case "reasoning":
|
|
// Flush any pending function calls before adding non-function content
|
|
flushPendingFunctionCalls()
|
|
|
|
// Add thinking content
|
|
if content := value.Get("content"); content.Exists() {
|
|
part := map[string]interface{}{
|
|
"thought": true,
|
|
"text": content.String(),
|
|
}
|
|
parts = append(parts, part)
|
|
}
|
|
|
|
case "message":
|
|
// Flush any pending function calls before adding non-function content
|
|
flushPendingFunctionCalls()
|
|
|
|
// Add regular text content
|
|
if content := value.Get("content"); content.Exists() && content.IsArray() {
|
|
content.ForEach(func(_, contentItem gjson.Result) bool {
|
|
if contentItem.Get("type").String() == "output_text" {
|
|
if text := contentItem.Get("text"); text.Exists() {
|
|
part := map[string]interface{}{
|
|
"text": text.String(),
|
|
}
|
|
parts = append(parts, part)
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
case "function_call":
|
|
// Collect function call for potential merging with consecutive ones
|
|
hasToolCall = true
|
|
functionCall := map[string]interface{}{
|
|
"functionCall": map[string]interface{}{
|
|
"name": value.Get("name").String(),
|
|
"args": map[string]interface{}{},
|
|
},
|
|
}
|
|
|
|
// Parse and set arguments
|
|
if argsStr := value.Get("arguments").String(); argsStr != "" {
|
|
argsResult := gjson.Parse(argsStr)
|
|
if argsResult.IsObject() {
|
|
var args map[string]interface{}
|
|
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
|
functionCall["functionCall"].(map[string]interface{})["args"] = args
|
|
}
|
|
}
|
|
}
|
|
|
|
pendingFunctionCalls = append(pendingFunctionCalls, functionCall)
|
|
}
|
|
return true
|
|
})
|
|
|
|
// Handle any remaining pending function calls at the end
|
|
flushPendingFunctionCalls()
|
|
}
|
|
|
|
// Set the parts array
|
|
if len(parts) > 0 {
|
|
template, _ = sjson.SetRaw(template, "candidates.0.content.parts", mustMarshalJSON(parts))
|
|
}
|
|
|
|
// Set finish reason based on whether there were tool calls
|
|
if hasToolCall {
|
|
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
|
} else {
|
|
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
|
}
|
|
}
|
|
|
|
return template
|
|
}
|
|
|
|
// mustMarshalJSON marshals data to JSON, panicking on error (should not happen with valid data)
|
|
func mustMarshalJSON(v interface{}) string {
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(data)
|
|
}
|