refactor: replace sjson.Set usage with sjson.SetBytes to optimize mutable JSON transformations

This commit is contained in:
Luis Pater
2026-03-19 17:58:54 +08:00
parent 56073ded69
commit 2bd646ad70
73 changed files with 3008 additions and 2944 deletions
@@ -12,6 +12,7 @@ import (
"strconv"
"strings"
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -44,8 +45,8 @@ type ToolCallAccumulator struct {
// - param: A pointer to a parameter object for the conversion.
//
// Returns:
// - []string: A slice of strings, each containing a Gemini-compatible JSON response.
func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
// - [][]byte: A slice of Gemini-compatible JSON responses.
func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
if *param == nil {
*param = &ConvertOpenAIResponseToGeminiParams{
ToolCallsAccumulator: nil,
@@ -55,8 +56,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
}
// Handle [DONE] marker
if strings.TrimSpace(string(rawJSON)) == "[DONE]" {
return []string{}
if bytes.Equal(bytes.TrimSpace(rawJSON), []byte("[DONE]")) {
return [][]byte{}
}
if bytes.HasPrefix(rawJSON, []byte("data:")) {
@@ -76,51 +77,51 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
if len(choices.Array()) == 0 {
// This is a usage-only chunk, handle usage and return
if usage := root.Get("usage"); usage.Exists() {
template := `{"candidates":[],"usageMetadata":{}}`
template := []byte(`{"candidates":[],"usageMetadata":{}}`)
// Set model if available
if model := root.Get("model"); model.Exists() {
template, _ = sjson.Set(template, "model", model.String())
template, _ = sjson.SetBytes(template, "model", model.String())
}
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
template, _ = sjson.SetBytes(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
}
return []string{template}
return [][]byte{template}
}
return []string{}
return [][]byte{}
}
var results []string
var results [][]byte
choices.ForEach(func(choiceIndex, choice gjson.Result) bool {
// Base Gemini response template without finishReason; set when known
template := `{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`
template := []byte(`{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`)
// Set model if available
if model := root.Get("model"); model.Exists() {
template, _ = sjson.Set(template, "model", model.String())
template, _ = sjson.SetBytes(template, "model", model.String())
}
_ = int(choice.Get("index").Int()) // choiceIdx not used in streaming
delta := choice.Get("delta")
baseTemplate := template
baseTemplate := append([]byte(nil), template...)
// Handle role (only in first chunk)
if role := delta.Get("role"); role.Exists() && (*param).(*ConvertOpenAIResponseToGeminiParams).IsFirstChunk {
// OpenAI assistant -> Gemini model
if role.String() == "assistant" {
template, _ = sjson.Set(template, "candidates.0.content.role", "model")
template, _ = sjson.SetBytes(template, "candidates.0.content.role", "model")
}
(*param).(*ConvertOpenAIResponseToGeminiParams).IsFirstChunk = false
results = append(results, template)
return true
}
var chunkOutputs []string
var chunkOutputs [][]byte
// Handle reasoning/thinking delta
if reasoning := delta.Get("reasoning_content"); reasoning.Exists() {
@@ -128,9 +129,9 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
if reasoningText == "" {
continue
}
reasoningTemplate := baseTemplate
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
reasoningTemplate := append([]byte(nil), baseTemplate...)
reasoningTemplate, _ = sjson.SetBytes(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
reasoningTemplate, _ = sjson.SetBytes(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
chunkOutputs = append(chunkOutputs, reasoningTemplate)
}
}
@@ -141,8 +142,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
(*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)
// Create text part for this delta
contentTemplate := baseTemplate
contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts.0.text", contentText)
contentTemplate := append([]byte(nil), baseTemplate...)
contentTemplate, _ = sjson.SetBytes(contentTemplate, "candidates.0.content.parts.0.text", contentText)
chunkOutputs = append(chunkOutputs, contentTemplate)
}
@@ -207,7 +208,7 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
// Handle finish reason
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
template, _ = sjson.Set(template, "candidates.0.finishReason", geminiFinishReason)
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", geminiFinishReason)
// If we have accumulated tool calls, output them now
if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {
@@ -215,8 +216,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {
namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
template, _ = sjson.Set(template, namePath, accumulator.Name)
template, _ = sjson.SetRaw(template, argsPath, parseArgsToObjectRaw(accumulator.Arguments.String()))
template, _ = sjson.SetBytes(template, namePath, accumulator.Name)
template, _ = sjson.SetRawBytes(template, argsPath, []byte(parseArgsToObjectRaw(accumulator.Arguments.String())))
partIndex++
}
@@ -230,11 +231,11 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
// Handle usage information
if usage := root.Get("usage"); usage.Exists() {
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
template, _ = sjson.SetBytes(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
}
results = append(results, template)
return true
@@ -244,7 +245,7 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
})
return results
}
return []string{}
return [][]byte{}
}
// mapOpenAIFinishReasonToGemini maps OpenAI finish reasons to Gemini finish reasons
@@ -310,7 +311,7 @@ func tolerantParseJSONObjectRaw(s string) string {
runes := []rune(content)
n := len(runes)
i := 0
result := "{}"
result := []byte(`{}`)
for i < n {
// Skip whitespace and commas
@@ -362,10 +363,10 @@ func tolerantParseJSONObjectRaw(s string) string {
valToken, ni := parseJSONStringRunes(runes, i)
if ni == -1 {
// Malformed; treat as empty string
result, _ = sjson.Set(result, sjsonKey, "")
result, _ = sjson.SetBytes(result, sjsonKey, "")
i = n
} else {
result, _ = sjson.Set(result, sjsonKey, jsonStringTokenToRawString(valToken))
result, _ = sjson.SetBytes(result, sjsonKey, jsonStringTokenToRawString(valToken))
i = ni
}
case '{', '[':
@@ -375,9 +376,9 @@ func tolerantParseJSONObjectRaw(s string) string {
i = n
} else {
if gjson.Valid(seg) {
result, _ = sjson.SetRaw(result, sjsonKey, seg)
result, _ = sjson.SetRawBytes(result, sjsonKey, []byte(seg))
} else {
result, _ = sjson.Set(result, sjsonKey, seg)
result, _ = sjson.SetBytes(result, sjsonKey, seg)
}
i = ni
}
@@ -390,15 +391,15 @@ func tolerantParseJSONObjectRaw(s string) string {
token := strings.TrimSpace(string(runes[i:j]))
// Interpret common JSON atoms and numbers; otherwise treat as string
if token == "true" {
result, _ = sjson.Set(result, sjsonKey, true)
result, _ = sjson.SetBytes(result, sjsonKey, true)
} else if token == "false" {
result, _ = sjson.Set(result, sjsonKey, false)
result, _ = sjson.SetBytes(result, sjsonKey, false)
} else if token == "null" {
result, _ = sjson.Set(result, sjsonKey, nil)
result, _ = sjson.SetBytes(result, sjsonKey, nil)
} else if numVal, ok := tryParseNumber(token); ok {
result, _ = sjson.Set(result, sjsonKey, numVal)
result, _ = sjson.SetBytes(result, sjsonKey, numVal)
} else {
result, _ = sjson.Set(result, sjsonKey, token)
result, _ = sjson.SetBytes(result, sjsonKey, token)
}
i = j
}
@@ -412,7 +413,7 @@ func tolerantParseJSONObjectRaw(s string) string {
}
}
return result
return string(result)
}
// parseJSONStringRunes returns the JSON string token (including quotes) and the index just after it.
@@ -531,16 +532,16 @@ func tryParseNumber(s string) (interface{}, bool) {
// - param: A pointer to a parameter object for the conversion.
//
// Returns:
// - string: A Gemini-compatible JSON response.
func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
// - []byte: A Gemini-compatible JSON response.
func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
root := gjson.ParseBytes(rawJSON)
// Base Gemini response template without finishReason; set when known
out := `{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`
out := []byte(`{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`)
// Set model if available
if model := root.Get("model"); model.Exists() {
out, _ = sjson.Set(out, "model", model.String())
out, _ = sjson.SetBytes(out, "model", model.String())
}
// Process choices
@@ -552,7 +553,7 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
// Set role
if role := message.Get("role"); role.Exists() {
if role.String() == "assistant" {
out, _ = sjson.Set(out, "candidates.0.content.role", "model")
out, _ = sjson.SetBytes(out, "candidates.0.content.role", "model")
}
}
@@ -564,15 +565,15 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
if reasoningText == "" {
continue
}
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true)
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText)
out, _ = sjson.SetBytes(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true)
out, _ = sjson.SetBytes(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText)
partIndex++
}
}
// Handle content first
if content := message.Get("content"); content.Exists() && content.String() != "" {
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String())
out, _ = sjson.SetBytes(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String())
partIndex++
}
@@ -586,8 +587,8 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
out, _ = sjson.Set(out, namePath, functionName)
out, _ = sjson.SetRaw(out, argsPath, parseArgsToObjectRaw(functionArgs))
out, _ = sjson.SetBytes(out, namePath, functionName)
out, _ = sjson.SetRawBytes(out, argsPath, []byte(parseArgsToObjectRaw(functionArgs)))
partIndex++
}
return true
@@ -597,11 +598,11 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
// Handle finish reason
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
out, _ = sjson.Set(out, "candidates.0.finishReason", geminiFinishReason)
out, _ = sjson.SetBytes(out, "candidates.0.finishReason", geminiFinishReason)
}
// Set index
out, _ = sjson.Set(out, "candidates.0.index", choiceIdx)
out, _ = sjson.SetBytes(out, "candidates.0.index", choiceIdx)
return true
})
@@ -609,19 +610,19 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
// Handle usage information
if usage := root.Get("usage"); usage.Exists() {
out, _ = sjson.Set(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
out, _ = sjson.Set(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
out, _ = sjson.Set(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
out, _ = sjson.SetBytes(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
out, _ = sjson.SetBytes(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
out, _ = sjson.SetBytes(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
out, _ = sjson.Set(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
out, _ = sjson.SetBytes(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
}
}
return out
}
func GeminiTokenCount(ctx context.Context, count int64) string {
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
func GeminiTokenCount(ctx context.Context, count int64) []byte {
return translatorcommon.GeminiTokenCountJSON(count)
}
func reasoningTokensFromUsage(usage gjson.Result) int64 {