mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-19 04:40:52 +08:00
refactor: replace json.Marshal and json.Unmarshal with sjson and gjson
Optimized the handling of JSON serialization and deserialization by replacing redundant `json.Marshal` and `json.Unmarshal` calls with `sjson` and `gjson`. Introduced a `marshalJSONValue` utility for compact JSON encoding, improving performance and code simplicity. Removed unused `encoding/json` imports.
This commit is contained in:
@@ -8,7 +8,6 @@ package gemini
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -84,15 +83,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
template, _ = sjson.Set(template, "model", model.String())
|
||||
}
|
||||
|
||||
usageObj := map[string]interface{}{
|
||||
"promptTokenCount": usage.Get("prompt_tokens").Int(),
|
||||
"candidatesTokenCount": usage.Get("completion_tokens").Int(),
|
||||
"totalTokenCount": usage.Get("total_tokens").Int(),
|
||||
}
|
||||
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())
|
||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||
usageObj["thoughtsTokenCount"] = reasoningTokens
|
||||
template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||
}
|
||||
template, _ = sjson.Set(template, "usageMetadata", usageObj)
|
||||
return []string{template}
|
||||
}
|
||||
return []string{}
|
||||
@@ -133,13 +129,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
continue
|
||||
}
|
||||
reasoningTemplate := baseTemplate
|
||||
parts := []interface{}{
|
||||
map[string]interface{}{
|
||||
"thought": true,
|
||||
"text": reasoningText,
|
||||
},
|
||||
}
|
||||
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts", parts)
|
||||
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
|
||||
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
|
||||
chunkOutputs = append(chunkOutputs, reasoningTemplate)
|
||||
}
|
||||
}
|
||||
@@ -150,13 +141,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
(*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)
|
||||
|
||||
// Create text part for this delta
|
||||
parts := []interface{}{
|
||||
map[string]interface{}{
|
||||
"text": contentText,
|
||||
},
|
||||
}
|
||||
contentTemplate := baseTemplate
|
||||
contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts", parts)
|
||||
contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts.0.text", contentText)
|
||||
chunkOutputs = append(chunkOutputs, contentTemplate)
|
||||
}
|
||||
|
||||
@@ -225,24 +211,13 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
|
||||
// If we have accumulated tool calls, output them now
|
||||
if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {
|
||||
var parts []interface{}
|
||||
partIndex := 0
|
||||
for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {
|
||||
argsStr := accumulator.Arguments.String()
|
||||
var argsMap map[string]interface{}
|
||||
|
||||
argsMap = parseArgsToMap(argsStr)
|
||||
|
||||
functionCallPart := map[string]interface{}{
|
||||
"functionCall": map[string]interface{}{
|
||||
"name": accumulator.Name,
|
||||
"args": argsMap,
|
||||
},
|
||||
}
|
||||
parts = append(parts, functionCallPart)
|
||||
}
|
||||
|
||||
if len(parts) > 0 {
|
||||
template, _ = sjson.Set(template, "candidates.0.content.parts", parts)
|
||||
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()))
|
||||
partIndex++
|
||||
}
|
||||
|
||||
// Clear accumulators
|
||||
@@ -255,15 +230,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
|
||||
// Handle usage information
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
usageObj := map[string]interface{}{
|
||||
"promptTokenCount": usage.Get("prompt_tokens").Int(),
|
||||
"candidatesTokenCount": usage.Get("completion_tokens").Int(),
|
||||
"totalTokenCount": usage.Get("total_tokens").Int(),
|
||||
}
|
||||
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())
|
||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||
usageObj["thoughtsTokenCount"] = reasoningTokens
|
||||
template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||
}
|
||||
template, _ = sjson.Set(template, "usageMetadata", usageObj)
|
||||
results = append(results, template)
|
||||
return true
|
||||
}
|
||||
@@ -291,46 +263,54 @@ func mapOpenAIFinishReasonToGemini(openAIReason string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// parseArgsToMap safely parses a JSON string of function arguments into a map.
|
||||
// It returns an empty map if the input is empty or cannot be parsed as a JSON object.
|
||||
func parseArgsToMap(argsStr string) map[string]interface{} {
|
||||
// parseArgsToObjectRaw safely parses a JSON string of function arguments into an object JSON string.
|
||||
// It returns "{}" if the input is empty or cannot be parsed as a JSON object.
|
||||
func parseArgsToObjectRaw(argsStr string) string {
|
||||
trimmed := strings.TrimSpace(argsStr)
|
||||
if trimmed == "" || trimmed == "{}" {
|
||||
return map[string]interface{}{}
|
||||
return "{}"
|
||||
}
|
||||
|
||||
// First try strict JSON
|
||||
var out map[string]interface{}
|
||||
if errUnmarshal := json.Unmarshal([]byte(trimmed), &out); errUnmarshal == nil {
|
||||
return out
|
||||
if gjson.Valid(trimmed) {
|
||||
strict := gjson.Parse(trimmed)
|
||||
if strict.IsObject() {
|
||||
return strict.Raw
|
||||
}
|
||||
}
|
||||
|
||||
// Tolerant parse: handle streams where values are barewords (e.g., 北京, celsius)
|
||||
tolerant := tolerantParseJSONMap(trimmed)
|
||||
if len(tolerant) > 0 {
|
||||
tolerant := tolerantParseJSONObjectRaw(trimmed)
|
||||
if tolerant != "{}" {
|
||||
return tolerant
|
||||
}
|
||||
|
||||
// Fallback: return empty object when parsing fails
|
||||
return map[string]interface{}{}
|
||||
return "{}"
|
||||
}
|
||||
|
||||
// tolerantParseJSONMap attempts to parse a JSON-like object string into a map, tolerating
|
||||
func escapeSjsonPathKey(key string) string {
|
||||
key = strings.ReplaceAll(key, `\`, `\\`)
|
||||
key = strings.ReplaceAll(key, `.`, `\.`)
|
||||
return key
|
||||
}
|
||||
|
||||
// tolerantParseJSONObjectRaw attempts to parse a JSON-like object string into a JSON object string, tolerating
|
||||
// bareword values (unquoted strings) commonly seen during streamed tool calls.
|
||||
// Example input: {"location": 北京, "unit": celsius}
|
||||
func tolerantParseJSONMap(s string) map[string]interface{} {
|
||||
func tolerantParseJSONObjectRaw(s string) string {
|
||||
// Ensure we operate within the outermost braces if present
|
||||
start := strings.Index(s, "{")
|
||||
end := strings.LastIndex(s, "}")
|
||||
if start == -1 || end == -1 || start >= end {
|
||||
return map[string]interface{}{}
|
||||
return "{}"
|
||||
}
|
||||
content := s[start+1 : end]
|
||||
|
||||
runes := []rune(content)
|
||||
n := len(runes)
|
||||
i := 0
|
||||
result := make(map[string]interface{})
|
||||
result := "{}"
|
||||
|
||||
for i < n {
|
||||
// Skip whitespace and commas
|
||||
@@ -356,6 +336,7 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
|
||||
break
|
||||
}
|
||||
keyName := jsonStringTokenToRawString(keyToken)
|
||||
sjsonKey := escapeSjsonPathKey(keyName)
|
||||
i = nextIdx
|
||||
|
||||
// Skip whitespace
|
||||
@@ -375,17 +356,16 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
|
||||
}
|
||||
|
||||
// Parse value (string, number, object/array, bareword)
|
||||
var value interface{}
|
||||
switch runes[i] {
|
||||
case '"':
|
||||
// JSON string
|
||||
valToken, ni := parseJSONStringRunes(runes, i)
|
||||
if ni == -1 {
|
||||
// Malformed; treat as empty string
|
||||
value = ""
|
||||
result, _ = sjson.Set(result, sjsonKey, "")
|
||||
i = n
|
||||
} else {
|
||||
value = jsonStringTokenToRawString(valToken)
|
||||
result, _ = sjson.Set(result, sjsonKey, jsonStringTokenToRawString(valToken))
|
||||
i = ni
|
||||
}
|
||||
case '{', '[':
|
||||
@@ -394,11 +374,10 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
|
||||
if ni == -1 {
|
||||
i = n
|
||||
} else {
|
||||
var anyVal interface{}
|
||||
if errUnmarshal := json.Unmarshal([]byte(seg), &anyVal); errUnmarshal == nil {
|
||||
value = anyVal
|
||||
if gjson.Valid(seg) {
|
||||
result, _ = sjson.SetRaw(result, sjsonKey, seg)
|
||||
} else {
|
||||
value = seg
|
||||
result, _ = sjson.Set(result, sjsonKey, seg)
|
||||
}
|
||||
i = ni
|
||||
}
|
||||
@@ -411,21 +390,19 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
|
||||
token := strings.TrimSpace(string(runes[i:j]))
|
||||
// Interpret common JSON atoms and numbers; otherwise treat as string
|
||||
if token == "true" {
|
||||
value = true
|
||||
result, _ = sjson.Set(result, sjsonKey, true)
|
||||
} else if token == "false" {
|
||||
value = false
|
||||
result, _ = sjson.Set(result, sjsonKey, false)
|
||||
} else if token == "null" {
|
||||
value = nil
|
||||
result, _ = sjson.Set(result, sjsonKey, nil)
|
||||
} else if numVal, ok := tryParseNumber(token); ok {
|
||||
value = numVal
|
||||
result, _ = sjson.Set(result, sjsonKey, numVal)
|
||||
} else {
|
||||
value = token
|
||||
result, _ = sjson.Set(result, sjsonKey, token)
|
||||
}
|
||||
i = j
|
||||
}
|
||||
|
||||
result[keyName] = value
|
||||
|
||||
// Skip trailing whitespace and optional comma before next pair
|
||||
for i < n && (runes[i] == ' ' || runes[i] == '\n' || runes[i] == '\r' || runes[i] == '\t') {
|
||||
i++
|
||||
@@ -463,9 +440,9 @@ func parseJSONStringRunes(runes []rune, start int) (string, int) {
|
||||
|
||||
// jsonStringTokenToRawString converts a JSON string token (including quotes) to a raw Go string value.
|
||||
func jsonStringTokenToRawString(token string) string {
|
||||
var s string
|
||||
if errUnmarshal := json.Unmarshal([]byte(token), &s); errUnmarshal == nil {
|
||||
return s
|
||||
r := gjson.Parse(token)
|
||||
if r.Type == gjson.String {
|
||||
return r.String()
|
||||
}
|
||||
// Fallback: strip surrounding quotes if present
|
||||
if len(token) >= 2 && token[0] == '"' && token[len(token)-1] == '"' {
|
||||
@@ -579,7 +556,7 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
||||
}
|
||||
}
|
||||
|
||||
var parts []interface{}
|
||||
partIndex := 0
|
||||
|
||||
// Handle reasoning content before visible text
|
||||
if reasoning := message.Get("reasoning_content"); reasoning.Exists() {
|
||||
@@ -587,18 +564,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
||||
if reasoningText == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, map[string]interface{}{
|
||||
"thought": true,
|
||||
"text": reasoningText,
|
||||
})
|
||||
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)
|
||||
partIndex++
|
||||
}
|
||||
}
|
||||
|
||||
// Handle content first
|
||||
if content := message.Get("content"); content.Exists() && content.String() != "" {
|
||||
parts = append(parts, map[string]interface{}{
|
||||
"text": content.String(),
|
||||
})
|
||||
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String())
|
||||
partIndex++
|
||||
}
|
||||
|
||||
// Handle tool calls
|
||||
@@ -609,27 +584,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
||||
functionName := function.Get("name").String()
|
||||
functionArgs := function.Get("arguments").String()
|
||||
|
||||
// Parse arguments
|
||||
var argsMap map[string]interface{}
|
||||
argsMap = parseArgsToMap(functionArgs)
|
||||
|
||||
functionCallPart := map[string]interface{}{
|
||||
"functionCall": map[string]interface{}{
|
||||
"name": functionName,
|
||||
"args": argsMap,
|
||||
},
|
||||
}
|
||||
parts = append(parts, functionCallPart)
|
||||
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))
|
||||
partIndex++
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Set parts
|
||||
if len(parts) > 0 {
|
||||
out, _ = sjson.Set(out, "candidates.0.content.parts", parts)
|
||||
}
|
||||
|
||||
// Handle finish reason
|
||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
|
||||
@@ -645,15 +609,12 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
||||
|
||||
// Handle usage information
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
usageObj := map[string]interface{}{
|
||||
"promptTokenCount": usage.Get("prompt_tokens").Int(),
|
||||
"candidatesTokenCount": usage.Get("completion_tokens").Int(),
|
||||
"totalTokenCount": usage.Get("total_tokens").Int(),
|
||||
}
|
||||
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())
|
||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||
usageObj["thoughtsTokenCount"] = reasoningTokens
|
||||
out, _ = sjson.Set(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||
}
|
||||
out, _ = sjson.Set(out, "usageMetadata", usageObj)
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user