Spaces:
Paused
Paused
| // Package gemini provides response translation functionality for Codex to Gemini API compatibility. | |
| // This package handles the conversion of Codex API responses into Gemini-compatible | |
| // JSON format, transforming streaming events and non-streaming responses into the format | |
| // expected by Gemini API clients. | |
| package gemini | |
| import ( | |
| "bytes" | |
| "context" | |
| "fmt" | |
| "time" | |
| "github.com/tidwall/gjson" | |
| "github.com/tidwall/sjson" | |
| ) | |
| var ( | |
| dataTag = []byte("data:") | |
| ) | |
| // ConvertCodexResponseToGeminiParams holds parameters for response conversion. | |
| type ConvertCodexResponseToGeminiParams struct { | |
| Model string | |
| CreatedAt int64 | |
| ResponseID string | |
| LastStorageOutput string | |
| } | |
| // ConvertCodexResponseToGemini converts Codex streaming response format to Gemini format. | |
| // This function processes various Codex event types and transforms them into Gemini-compatible JSON responses. | |
| // It handles text content, tool calls, and usage metadata, outputting responses that match the Gemini API format. | |
| // The function maintains state across multiple calls to ensure proper response sequencing. | |
| // | |
| // Parameters: | |
| // - ctx: The context for the request, used for cancellation and timeout handling | |
| // - modelName: The name of the model being used for the response | |
| // - rawJSON: The raw JSON response from the Codex API | |
| // - param: A pointer to a parameter object for maintaining state between calls | |
| // | |
| // Returns: | |
| // - []string: A slice of strings, each containing a Gemini-compatible JSON response | |
| func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { | |
| if *param == nil { | |
| *param = &ConvertCodexResponseToGeminiParams{ | |
| Model: modelName, | |
| CreatedAt: 0, | |
| ResponseID: "", | |
| LastStorageOutput: "", | |
| } | |
| } | |
| if !bytes.HasPrefix(rawJSON, dataTag) { | |
| return []string{} | |
| } | |
| rawJSON = bytes.TrimSpace(rawJSON[5:]) | |
| 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).(*ConvertCodexResponseToGeminiParams).LastStorageOutput != "" && typeStr == "response.output_item.done" { | |
| template = (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput | |
| } else { | |
| template, _ = sjson.Set(template, "modelVersion", (*param).(*ConvertCodexResponseToGeminiParams).Model) | |
| createdAtResult := rootResult.Get("response.created_at") | |
| if createdAtResult.Exists() { | |
| (*param).(*ConvertCodexResponseToGeminiParams).CreatedAt = createdAtResult.Int() | |
| template, _ = sjson.Set(template, "createTime", time.Unix((*param).(*ConvertCodexResponseToGeminiParams).CreatedAt, 0).Format(time.RFC3339Nano)) | |
| } | |
| template, _ = sjson.Set(template, "responseId", (*param).(*ConvertCodexResponseToGeminiParams).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":{}}}` | |
| { | |
| // Restore original tool name if shortened | |
| n := itemResult.Get("name").String() | |
| rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON) | |
| if orig, ok := rev[n]; ok { | |
| n = orig | |
| } | |
| functionCall, _ = sjson.Set(functionCall, "functionCall.name", n) | |
| } | |
| // 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).(*ConvertCodexResponseToGeminiParams).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).(*ConvertCodexResponseToGeminiParams).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).(*ConvertCodexResponseToGeminiParams).LastStorageOutput != "" { | |
| return []string{(*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput, template} | |
| } else { | |
| return []string{template} | |
| } | |
| } | |
| // ConvertCodexResponseToGeminiNonStream converts a non-streaming Codex response to a non-streaming Gemini response. | |
| // This function processes the complete Codex response and transforms it into a single Gemini-compatible | |
| // JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all | |
| // the information into a single response that matches the Gemini API format. | |
| // | |
| // Parameters: | |
| // - ctx: The context for the request, used for cancellation and timeout handling | |
| // - modelName: The name of the model being used for the response | |
| // - rawJSON: The raw JSON response from the Codex API | |
| // - param: A pointer to a parameter object for the conversion (unused in current implementation) | |
| // | |
| // Returns: | |
| // - string: A Gemini-compatible JSON response containing all message content and metadata | |
| func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) 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", modelName) | |
| // 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 | |
| hasToolCall := false | |
| var pendingFunctionCalls []string | |
| flushPendingFunctionCalls := func() { | |
| if len(pendingFunctionCalls) == 0 { | |
| return | |
| } | |
| // 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 { | |
| template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", 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 := `{"text":"","thought":true}` | |
| part, _ = sjson.Set(part, "text", content.String()) | |
| template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", 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 := `{"text":""}` | |
| part, _ = sjson.Set(part, "text", text.String()) | |
| template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part) | |
| } | |
| } | |
| return true | |
| }) | |
| } | |
| case "function_call": | |
| // Collect function call for potential merging with consecutive ones | |
| hasToolCall = true | |
| functionCall := `{"functionCall":{"args":{},"name":""}}` | |
| { | |
| n := value.Get("name").String() | |
| rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON) | |
| if orig, ok := rev[n]; ok { | |
| n = orig | |
| } | |
| functionCall, _ = sjson.Set(functionCall, "functionCall.name", n) | |
| } | |
| // Parse and set arguments | |
| if argsStr := value.Get("arguments").String(); argsStr != "" { | |
| argsResult := gjson.Parse(argsStr) | |
| if argsResult.IsObject() { | |
| functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsStr) | |
| } | |
| } | |
| pendingFunctionCalls = append(pendingFunctionCalls, functionCall) | |
| } | |
| return true | |
| }) | |
| // Handle any remaining pending function calls at the end | |
| flushPendingFunctionCalls() | |
| } | |
| // 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 | |
| } | |
| // buildReverseMapFromGeminiOriginal builds a map[short]original from original Gemini request tools. | |
| func buildReverseMapFromGeminiOriginal(original []byte) map[string]string { | |
| tools := gjson.GetBytes(original, "tools") | |
| rev := map[string]string{} | |
| if !tools.IsArray() { | |
| return rev | |
| } | |
| var names []string | |
| tarr := tools.Array() | |
| for i := 0; i < len(tarr); i++ { | |
| fns := tarr[i].Get("functionDeclarations") | |
| if !fns.IsArray() { | |
| continue | |
| } | |
| for _, fn := range fns.Array() { | |
| if v := fn.Get("name"); v.Exists() { | |
| names = append(names, v.String()) | |
| } | |
| } | |
| } | |
| if len(names) > 0 { | |
| m := buildShortNameMap(names) | |
| for orig, short := range m { | |
| rev[short] = orig | |
| } | |
| } | |
| return rev | |
| } | |
| func GeminiTokenCount(ctx context.Context, count int64) string { | |
| return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) | |
| } | |