fix: extend tool name sanitization to all remaining Gemini-bound translators
Apply SanitizeFunctionName on request and RestoreSanitizedToolName on response for: gemini/claude, gemini/openai/chat-completions, gemini/openai/responses, antigravity/openai/chat-completions, gemini-cli/openai/chat-completions. Also update SanitizedToolNameMap to handle OpenAI format (tools[].function.name) in addition to Claude format (tools[].name).
This commit is contained in:
@@ -286,7 +286,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fid := tc.Get("id").String()
|
fid := tc.Get("id").String()
|
||||||
fname := tc.Get("function.name").String()
|
fname := util.SanitizeFunctionName(tc.Get("function.name").String())
|
||||||
fargs := tc.Get("function.arguments").String()
|
fargs := tc.Get("function.arguments").String()
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid)
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid)
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
||||||
@@ -309,7 +309,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
for _, fid := range fIDs {
|
for _, fid := range fIDs {
|
||||||
if name, ok := tcID2Name[fid]; ok {
|
if name, ok := tcID2Name[fid]; ok {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid)
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid)
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", util.SanitizeFunctionName(name))
|
||||||
resp := toolResponses[fid]
|
resp := toolResponses[fid]
|
||||||
if resp == "" {
|
if resp == "" {
|
||||||
resp = "{}"
|
resp = "{}"
|
||||||
@@ -384,7 +384,9 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
}
|
}
|
||||||
fnRaw = string(fnRawBytes)
|
fnRaw = string(fnRawBytes)
|
||||||
}
|
}
|
||||||
fnRaw, _ = sjson.Delete(fnRaw, "strict")
|
fnRawBytes := []byte(fnRaw)
|
||||||
|
fnRawBytes, _ = sjson.SetBytes(fnRawBytes, "name", util.SanitizeFunctionName(fn.Get("name").String()))
|
||||||
|
fnRaw, _ = sjson.Delete(string(fnRawBytes), "strict")
|
||||||
if !hasFunction {
|
if !hasFunction {
|
||||||
functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations", []byte("[]"))
|
functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations", []byte("[]"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions"
|
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions"
|
||||||
@@ -26,6 +27,7 @@ type convertCliResponseToOpenAIChatParams struct {
|
|||||||
FunctionIndex int
|
FunctionIndex int
|
||||||
SawToolCall bool // Tracks if any tool call was seen in the entire stream
|
SawToolCall bool // Tracks if any tool call was seen in the entire stream
|
||||||
UpstreamFinishReason string // Caches the upstream finish reason for final chunk
|
UpstreamFinishReason string // Caches the upstream finish reason for final chunk
|
||||||
|
SanitizedNameMap map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// functionCallIDCounter provides a process-wide unique counter for function call identifiers.
|
// functionCallIDCounter provides a process-wide unique counter for function call identifiers.
|
||||||
@@ -48,10 +50,14 @@ var functionCallIDCounter uint64
|
|||||||
func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &convertCliResponseToOpenAIChatParams{
|
*param = &convertCliResponseToOpenAIChatParams{
|
||||||
UnixTimestamp: 0,
|
UnixTimestamp: 0,
|
||||||
FunctionIndex: 0,
|
FunctionIndex: 0,
|
||||||
|
SanitizedNameMap: util.SanitizedToolNameMap(originalRequestRawJSON),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (*param).(*convertCliResponseToOpenAIChatParams).SanitizedNameMap == nil {
|
||||||
|
(*param).(*convertCliResponseToOpenAIChatParams).SanitizedNameMap = util.SanitizedToolNameMap(originalRequestRawJSON)
|
||||||
|
}
|
||||||
|
|
||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
return [][]byte{}
|
return [][]byte{}
|
||||||
@@ -159,7 +165,7 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
functionCallTemplate := []byte(`{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}`)
|
functionCallTemplate := []byte(`{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}`)
|
||||||
fcName := functionCallResult.Get("name").String()
|
fcName := util.RestoreSanitizedToolName((*param).(*convertCliResponseToOpenAIChatParams).SanitizedNameMap, functionCallResult.Get("name").String())
|
||||||
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
||||||
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex)
|
||||||
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName)
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fid := tc.Get("id").String()
|
fid := tc.Get("id").String()
|
||||||
fname := tc.Get("function.name").String()
|
fname := util.SanitizeFunctionName(tc.Get("function.name").String())
|
||||||
fargs := tc.Get("function.arguments").String()
|
fargs := tc.Get("function.arguments").String()
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
||||||
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
||||||
@@ -268,7 +268,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
pp := 0
|
pp := 0
|
||||||
for _, fid := range fIDs {
|
for _, fid := range fIDs {
|
||||||
if name, ok := tcID2Name[fid]; ok {
|
if name, ok := tcID2Name[fid]; ok {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", util.SanitizeFunctionName(name))
|
||||||
resp := toolResponses[fid]
|
resp := toolResponses[fid]
|
||||||
if resp == "" {
|
if resp == "" {
|
||||||
resp = "{}"
|
resp = "{}"
|
||||||
@@ -331,6 +331,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fnRaw, _ = sjson.SetBytes(fnRaw, "name", util.SanitizeFunctionName(fn.Get("name").String()))
|
||||||
fnRaw, _ = sjson.DeleteBytes(fnRaw, "strict")
|
fnRaw, _ = sjson.DeleteBytes(fnRaw, "strict")
|
||||||
if !hasFunction {
|
if !hasFunction {
|
||||||
functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations", []byte("[]"))
|
functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations", []byte("[]"))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions"
|
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -21,8 +22,9 @@ import (
|
|||||||
|
|
||||||
// convertCliResponseToOpenAIChatParams holds parameters for response conversion.
|
// convertCliResponseToOpenAIChatParams holds parameters for response conversion.
|
||||||
type convertCliResponseToOpenAIChatParams struct {
|
type convertCliResponseToOpenAIChatParams struct {
|
||||||
UnixTimestamp int64
|
UnixTimestamp int64
|
||||||
FunctionIndex int
|
FunctionIndex int
|
||||||
|
SanitizedNameMap map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// functionCallIDCounter provides a process-wide unique counter for function call identifiers.
|
// functionCallIDCounter provides a process-wide unique counter for function call identifiers.
|
||||||
@@ -45,10 +47,14 @@ var functionCallIDCounter uint64
|
|||||||
func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &convertCliResponseToOpenAIChatParams{
|
*param = &convertCliResponseToOpenAIChatParams{
|
||||||
UnixTimestamp: 0,
|
UnixTimestamp: 0,
|
||||||
FunctionIndex: 0,
|
FunctionIndex: 0,
|
||||||
|
SanitizedNameMap: util.SanitizedToolNameMap(originalRequestRawJSON),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (*param).(*convertCliResponseToOpenAIChatParams).SanitizedNameMap == nil {
|
||||||
|
(*param).(*convertCliResponseToOpenAIChatParams).SanitizedNameMap = util.SanitizedToolNameMap(originalRequestRawJSON)
|
||||||
|
}
|
||||||
|
|
||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
return [][]byte{}
|
return [][]byte{}
|
||||||
@@ -163,7 +169,7 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
|
|||||||
}
|
}
|
||||||
|
|
||||||
functionCallTemplate := []byte(`{"id":"","index":0,"type":"function","function":{"name":"","arguments":""}}`)
|
functionCallTemplate := []byte(`{"id":"","index":0,"type":"function","function":{"name":"","arguments":""}}`)
|
||||||
fcName := functionCallResult.Get("name").String()
|
fcName := util.RestoreSanitizedToolName((*param).(*convertCliResponseToOpenAIChatParams).SanitizedNameMap, functionCallResult.Get("name").String())
|
||||||
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
||||||
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex)
|
||||||
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -90,6 +91,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
functionName = derived
|
functionName = derived
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
functionName = util.SanitizeFunctionName(functionName)
|
||||||
functionArgs := contentResult.Get("input").String()
|
functionArgs := contentResult.Get("input").String()
|
||||||
argsResult := gjson.Parse(functionArgs)
|
argsResult := gjson.Parse(functionArgs)
|
||||||
if argsResult.IsObject() && gjson.Valid(functionArgs) {
|
if argsResult.IsObject() && gjson.Valid(functionArgs) {
|
||||||
@@ -109,6 +111,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
if funcName == "" {
|
if funcName == "" {
|
||||||
funcName = toolCallID
|
funcName = toolCallID
|
||||||
}
|
}
|
||||||
|
funcName = util.SanitizeFunctionName(funcName)
|
||||||
responseData := contentResult.Get("content").Raw
|
responseData := contentResult.Get("content").Raw
|
||||||
part := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`)
|
part := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`)
|
||||||
part, _ = sjson.SetBytes(part, "functionResponse.name", funcName)
|
part, _ = sjson.SetBytes(part, "functionResponse.name", funcName)
|
||||||
@@ -165,6 +168,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
tool, _ = sjson.DeleteBytes(tool, "type")
|
tool, _ = sjson.DeleteBytes(tool, "type")
|
||||||
tool, _ = sjson.DeleteBytes(tool, "cache_control")
|
tool, _ = sjson.DeleteBytes(tool, "cache_control")
|
||||||
tool, _ = sjson.DeleteBytes(tool, "defer_loading")
|
tool, _ = sjson.DeleteBytes(tool, "defer_loading")
|
||||||
|
tool, _ = sjson.SetBytes(tool, "name", util.SanitizeFunctionName(gjson.GetBytes(tool, "name").String()))
|
||||||
if gjson.ValidBytes(tool) && gjson.ParseBytes(tool).IsObject() {
|
if gjson.ValidBytes(tool) && gjson.ParseBytes(tool).IsObject() {
|
||||||
if !hasTools {
|
if !hasTools {
|
||||||
out, _ = sjson.SetRawBytes(out, "tools", []byte(`[{"functionDeclarations":[]}]`))
|
out, _ = sjson.SetRawBytes(out, "tools", []byte(`[{"functionDeclarations":[]}]`))
|
||||||
@@ -202,7 +206,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
case "tool":
|
case "tool":
|
||||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "ANY")
|
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "ANY")
|
||||||
if toolChoiceName != "" {
|
if toolChoiceName != "" {
|
||||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.allowedFunctionNames", []string{toolChoiceName})
|
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.allowedFunctionNames", []string{util.SanitizeFunctionName(toolChoiceName)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type Params struct {
|
|||||||
ResponseIndex int
|
ResponseIndex int
|
||||||
HasContent bool // Tracks whether any content (text, thinking, or tool use) has been output
|
HasContent bool // Tracks whether any content (text, thinking, or tool use) has been output
|
||||||
ToolNameMap map[string]string
|
ToolNameMap map[string]string
|
||||||
|
SanitizedNameMap map[string]string
|
||||||
SawToolCall bool
|
SawToolCall bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
ResponseType: 0,
|
ResponseType: 0,
|
||||||
ResponseIndex: 0,
|
ResponseIndex: 0,
|
||||||
ToolNameMap: util.ToolNameMapFromClaudeRequest(originalRequestRawJSON),
|
ToolNameMap: util.ToolNameMapFromClaudeRequest(originalRequestRawJSON),
|
||||||
|
SanitizedNameMap: util.SanitizedToolNameMap(originalRequestRawJSON),
|
||||||
SawToolCall: false,
|
SawToolCall: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +169,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
// This processes tool usage requests and formats them for Claude API compatibility
|
// This processes tool usage requests and formats them for Claude API compatibility
|
||||||
(*param).(*Params).SawToolCall = true
|
(*param).(*Params).SawToolCall = true
|
||||||
upstreamToolName := functionCallResult.Get("name").String()
|
upstreamToolName := functionCallResult.Get("name").String()
|
||||||
|
upstreamToolName = util.RestoreSanitizedToolName((*param).(*Params).SanitizedNameMap, upstreamToolName)
|
||||||
clientToolName := util.MapToolName((*param).(*Params).ToolNameMap, upstreamToolName)
|
clientToolName := util.MapToolName((*param).(*Params).ToolNameMap, upstreamToolName)
|
||||||
|
|
||||||
// FIX: Handle streaming split/delta where name might be empty in subsequent chunks.
|
// FIX: Handle streaming split/delta where name might be empty in subsequent chunks.
|
||||||
@@ -260,6 +263,7 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
toolNameMap := util.ToolNameMapFromClaudeRequest(originalRequestRawJSON)
|
toolNameMap := util.ToolNameMapFromClaudeRequest(originalRequestRawJSON)
|
||||||
|
sanitizedNameMap := util.SanitizedToolNameMap(originalRequestRawJSON)
|
||||||
|
|
||||||
out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`)
|
out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`)
|
||||||
out, _ = sjson.SetBytes(out, "id", root.Get("responseId").String())
|
out, _ = sjson.SetBytes(out, "id", root.Get("responseId").String())
|
||||||
@@ -315,6 +319,7 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
|
|
||||||
upstreamToolName := functionCall.Get("name").String()
|
upstreamToolName := functionCall.Get("name").String()
|
||||||
|
upstreamToolName = util.RestoreSanitizedToolName(sanitizedNameMap, upstreamToolName)
|
||||||
clientToolName := util.MapToolName(toolNameMap, upstreamToolName)
|
clientToolName := util.MapToolName(toolNameMap, upstreamToolName)
|
||||||
toolIDCounter++
|
toolIDCounter++
|
||||||
toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fid := tc.Get("id").String()
|
fid := tc.Get("id").String()
|
||||||
fname := tc.Get("function.name").String()
|
fname := util.SanitizeFunctionName(tc.Get("function.name").String())
|
||||||
fargs := tc.Get("function.arguments").String()
|
fargs := tc.Get("function.arguments").String()
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
||||||
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
||||||
@@ -274,7 +274,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
pp := 0
|
pp := 0
|
||||||
for _, fid := range fIDs {
|
for _, fid := range fIDs {
|
||||||
if name, ok := tcID2Name[fid]; ok {
|
if name, ok := tcID2Name[fid]; ok {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", util.SanitizeFunctionName(name))
|
||||||
resp := toolResponses[fid]
|
resp := toolResponses[fid]
|
||||||
if resp == "" {
|
if resp == "" {
|
||||||
resp = "{}"
|
resp = "{}"
|
||||||
@@ -341,6 +341,9 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
fnRaw = string(fnRawBytes)
|
fnRaw = string(fnRawBytes)
|
||||||
}
|
}
|
||||||
|
fnRawBytes := []byte(fnRaw)
|
||||||
|
fnRawBytes, _ = sjson.SetBytes(fnRawBytes, "name", util.SanitizeFunctionName(fn.Get("name").String()))
|
||||||
|
fnRaw = string(fnRawBytes)
|
||||||
fnRaw, _ = sjson.Delete(fnRaw, "strict")
|
fnRaw, _ = sjson.Delete(fnRaw, "strict")
|
||||||
if !hasFunction {
|
if !hasFunction {
|
||||||
functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations", []byte("[]"))
|
functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations", []byte("[]"))
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -22,7 +23,8 @@ import (
|
|||||||
type convertGeminiResponseToOpenAIChatParams struct {
|
type convertGeminiResponseToOpenAIChatParams struct {
|
||||||
UnixTimestamp int64
|
UnixTimestamp int64
|
||||||
// FunctionIndex tracks tool call indices per candidate index to support multiple candidates.
|
// FunctionIndex tracks tool call indices per candidate index to support multiple candidates.
|
||||||
FunctionIndex map[int]int
|
FunctionIndex map[int]int
|
||||||
|
SanitizedNameMap map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// functionCallIDCounter provides a process-wide unique counter for function call identifiers.
|
// functionCallIDCounter provides a process-wide unique counter for function call identifiers.
|
||||||
@@ -46,8 +48,9 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
// Initialize parameters if nil.
|
// Initialize parameters if nil.
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &convertGeminiResponseToOpenAIChatParams{
|
*param = &convertGeminiResponseToOpenAIChatParams{
|
||||||
UnixTimestamp: 0,
|
UnixTimestamp: 0,
|
||||||
FunctionIndex: make(map[int]int),
|
FunctionIndex: make(map[int]int),
|
||||||
|
SanitizedNameMap: util.SanitizedToolNameMap(originalRequestRawJSON),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +59,9 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
if p.FunctionIndex == nil {
|
if p.FunctionIndex == nil {
|
||||||
p.FunctionIndex = make(map[int]int)
|
p.FunctionIndex = make(map[int]int)
|
||||||
}
|
}
|
||||||
|
if p.SanitizedNameMap == nil {
|
||||||
|
p.SanitizedNameMap = util.SanitizedToolNameMap(originalRequestRawJSON)
|
||||||
|
}
|
||||||
|
|
||||||
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
@@ -191,7 +197,7 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
}
|
}
|
||||||
|
|
||||||
functionCallTemplate := []byte(`{"id":"","index":0,"type":"function","function":{"name":"","arguments":""}}`)
|
functionCallTemplate := []byte(`{"id":"","index":0,"type":"function","function":{"name":"","arguments":""}}`)
|
||||||
fcName := functionCallResult.Get("name").String()
|
fcName := util.RestoreSanitizedToolName(p.SanitizedNameMap, functionCallResult.Get("name").String())
|
||||||
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
||||||
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex)
|
||||||
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName)
|
||||||
@@ -265,6 +271,7 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: An OpenAI-compatible JSON response containing all message content and metadata
|
// - []byte: An OpenAI-compatible JSON response containing all message content and metadata
|
||||||
func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
|
sanitizedNameMap := util.SanitizedToolNameMap(originalRequestRawJSON)
|
||||||
var unixTimestamp int64
|
var unixTimestamp int64
|
||||||
// Initialize template with an empty choices array to support multiple candidates.
|
// Initialize template with an empty choices array to support multiple candidates.
|
||||||
template := []byte(`{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[]}`)
|
template := []byte(`{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[]}`)
|
||||||
@@ -358,7 +365,7 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
choiceTemplate, _ = sjson.SetRawBytes(choiceTemplate, "message.tool_calls", []byte(`[]`))
|
choiceTemplate, _ = sjson.SetRawBytes(choiceTemplate, "message.tool_calls", []byte(`[]`))
|
||||||
}
|
}
|
||||||
functionCallItemTemplate := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`)
|
functionCallItemTemplate := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`)
|
||||||
fcName := functionCallResult.Get("name").String()
|
fcName := util.RestoreSanitizedToolName(sanitizedNameMap, functionCallResult.Get("name").String())
|
||||||
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
||||||
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.name", fcName)
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.name", fcName)
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -291,7 +292,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
|
|
||||||
case "function_call":
|
case "function_call":
|
||||||
// Handle function calls - convert to model message with functionCall
|
// Handle function calls - convert to model message with functionCall
|
||||||
name := item.Get("name").String()
|
name := util.SanitizeFunctionName(item.Get("name").String())
|
||||||
arguments := item.Get("arguments").String()
|
arguments := item.Get("arguments").String()
|
||||||
|
|
||||||
modelContent := []byte(`{"role":"model","parts":[]}`)
|
modelContent := []byte(`{"role":"model","parts":[]}`)
|
||||||
@@ -333,6 +334,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
functionName = util.SanitizeFunctionName(functionName)
|
||||||
|
|
||||||
functionResponse, _ = sjson.SetBytes(functionResponse, "functionResponse.name", functionName)
|
functionResponse, _ = sjson.SetBytes(functionResponse, "functionResponse.name", functionName)
|
||||||
functionResponse, _ = sjson.SetBytes(functionResponse, "functionResponse.id", callID)
|
functionResponse, _ = sjson.SetBytes(functionResponse, "functionResponse.id", callID)
|
||||||
@@ -375,7 +377,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
funcDecl := []byte(`{"name":"","description":"","parametersJsonSchema":{}}`)
|
funcDecl := []byte(`{"name":"","description":"","parametersJsonSchema":{}}`)
|
||||||
|
|
||||||
if name := tool.Get("name"); name.Exists() {
|
if name := tool.Get("name"); name.Exists() {
|
||||||
funcDecl, _ = sjson.SetBytes(funcDecl, "name", name.String())
|
funcDecl, _ = sjson.SetBytes(funcDecl, "name", util.SanitizeFunctionName(name.String()))
|
||||||
}
|
}
|
||||||
if desc := tool.Get("description"); desc.Exists() {
|
if desc := tool.Get("description"); desc.Exists() {
|
||||||
funcDecl, _ = sjson.SetBytes(funcDecl, "description", desc.String())
|
funcDecl, _ = sjson.SetBytes(funcDecl, "description", desc.String())
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -36,11 +37,12 @@ type geminiToResponsesState struct {
|
|||||||
ReasoningClosed bool
|
ReasoningClosed bool
|
||||||
|
|
||||||
// function call aggregation (keyed by output_index)
|
// function call aggregation (keyed by output_index)
|
||||||
NextIndex int
|
NextIndex int
|
||||||
FuncArgsBuf map[int]*strings.Builder
|
FuncArgsBuf map[int]*strings.Builder
|
||||||
FuncNames map[int]string
|
FuncNames map[int]string
|
||||||
FuncCallIDs map[int]string
|
FuncCallIDs map[int]string
|
||||||
FuncDone map[int]bool
|
FuncDone map[int]bool
|
||||||
|
SanitizedNameMap map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// responseIDCounter provides a process-wide unique counter for synthesized response identifiers.
|
// responseIDCounter provides a process-wide unique counter for synthesized response identifiers.
|
||||||
@@ -90,10 +92,11 @@ func emitEvent(event string, payload []byte) []byte {
|
|||||||
func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &geminiToResponsesState{
|
*param = &geminiToResponsesState{
|
||||||
FuncArgsBuf: make(map[int]*strings.Builder),
|
FuncArgsBuf: make(map[int]*strings.Builder),
|
||||||
FuncNames: make(map[int]string),
|
FuncNames: make(map[int]string),
|
||||||
FuncCallIDs: make(map[int]string),
|
FuncCallIDs: make(map[int]string),
|
||||||
FuncDone: make(map[int]bool),
|
FuncDone: make(map[int]bool),
|
||||||
|
SanitizedNameMap: util.SanitizedToolNameMap(originalRequestRawJSON),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
st := (*param).(*geminiToResponsesState)
|
st := (*param).(*geminiToResponsesState)
|
||||||
@@ -109,6 +112,9 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
if st.FuncDone == nil {
|
if st.FuncDone == nil {
|
||||||
st.FuncDone = make(map[int]bool)
|
st.FuncDone = make(map[int]bool)
|
||||||
}
|
}
|
||||||
|
if st.SanitizedNameMap == nil {
|
||||||
|
st.SanitizedNameMap = util.SanitizedToolNameMap(originalRequestRawJSON)
|
||||||
|
}
|
||||||
|
|
||||||
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
@@ -306,7 +312,7 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
// Responses streaming requires message done events before the next output_item.added.
|
// Responses streaming requires message done events before the next output_item.added.
|
||||||
finalizeReasoning()
|
finalizeReasoning()
|
||||||
finalizeMessage()
|
finalizeMessage()
|
||||||
name := fc.Get("name").String()
|
name := util.RestoreSanitizedToolName(st.SanitizedNameMap, fc.Get("name").String())
|
||||||
idx := st.NextIndex
|
idx := st.NextIndex
|
||||||
st.NextIndex++
|
st.NextIndex++
|
||||||
// Ensure buffers
|
// Ensure buffers
|
||||||
@@ -565,6 +571,7 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
root = unwrapGeminiResponseRoot(root)
|
root = unwrapGeminiResponseRoot(root)
|
||||||
|
sanitizedNameMap := util.SanitizedToolNameMap(originalRequestRawJSON)
|
||||||
|
|
||||||
// Base response scaffold
|
// Base response scaffold
|
||||||
resp := []byte(`{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null}`)
|
resp := []byte(`{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null}`)
|
||||||
@@ -694,7 +701,7 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if fc := p.Get("functionCall"); fc.Exists() {
|
if fc := p.Get("functionCall"); fc.Exists() {
|
||||||
name := fc.Get("name").String()
|
name := util.RestoreSanitizedToolName(sanitizedNameMap, fc.Get("name").String())
|
||||||
args := fc.Get("args")
|
args := fc.Get("args")
|
||||||
callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1))
|
callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1))
|
||||||
itemJSON := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
|
itemJSON := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
|
||||||
|
|||||||
@@ -244,6 +244,9 @@ func ToolNameMapFromClaudeRequest(rawJSON []byte) map[string]string {
|
|||||||
out := make(map[string]string, len(toolResults))
|
out := make(map[string]string, len(toolResults))
|
||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
name := strings.TrimSpace(tool.Get("name").String())
|
name := strings.TrimSpace(tool.Get("name").String())
|
||||||
|
if name == "" {
|
||||||
|
name = strings.TrimSpace(tool.Get("function.name").String())
|
||||||
|
}
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user