| |
| |
| |
| |
| package gemini |
|
|
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "time" |
|
|
| "github.com/tidwall/gjson" |
| "github.com/tidwall/sjson" |
| ) |
|
|
| var ( |
| dataTag = []byte("data:") |
| ) |
|
|
| |
| type ConvertCodexResponseToGeminiParams struct { |
| Model string |
| CreatedAt int64 |
| ResponseID string |
| LastStorageOutput string |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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() |
|
|
| |
| 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) |
| } |
|
|
| |
| if typeStr == "response.output_item.done" { |
| itemResult := rootResult.Get("item") |
| itemType := itemResult.Get("type").String() |
| if itemType == "function_call" { |
| |
| functionCall := `{"functionCall":{"name":"","args":{}}}` |
| { |
| |
| n := itemResult.Get("name").String() |
| rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON) |
| if orig, ok := rev[n]; ok { |
| n = orig |
| } |
| functionCall, _ = sjson.Set(functionCall, "functionCall.name", n) |
| } |
|
|
| |
| 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 |
|
|
| |
| return []string{} |
| } |
| } |
|
|
| if typeStr == "response.created" { |
| 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" { |
| 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" { |
| 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" { |
| 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} |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { |
| rootResult := gjson.ParseBytes(rawJSON) |
|
|
| |
| if rootResult.Get("type").String() != "response.completed" { |
| return "" |
| } |
|
|
| |
| template := `{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}` |
|
|
| |
| template, _ = sjson.Set(template, "modelVersion", modelName) |
|
|
| |
| responseData := rootResult.Get("response") |
| if responseData.Exists() { |
| |
| if responseId := responseData.Get("id"); responseId.Exists() { |
| template, _ = sjson.Set(template, "responseId", responseId.String()) |
| } |
|
|
| |
| if createdAt := responseData.Get("created_at"); createdAt.Exists() { |
| template, _ = sjson.Set(template, "createTime", time.Unix(createdAt.Int(), 0).Format(time.RFC3339Nano)) |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| hasToolCall := false |
| var pendingFunctionCalls []string |
|
|
| flushPendingFunctionCalls := func() { |
| if len(pendingFunctionCalls) == 0 { |
| return |
| } |
| |
| |
| 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": |
| |
| flushPendingFunctionCalls() |
|
|
| |
| 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": |
| |
| flushPendingFunctionCalls() |
|
|
| |
| 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": |
| |
| 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) |
| } |
|
|
| |
| 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 |
| }) |
|
|
| |
| flushPendingFunctionCalls() |
| } |
|
|
| |
| if hasToolCall { |
| template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") |
| } else { |
| template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") |
| } |
| } |
| return template |
| } |
|
|
| |
| 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) |
| } |
|
|