mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
Fixed: #551
fix(translator): standardize content node handling across translators for assistant and tool calls
This commit is contained in:
@@ -222,62 +222,61 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
}
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
} else if role == "assistant" {
|
} else if role == "assistant" {
|
||||||
|
node := []byte(`{"role":"model","parts":[]}`)
|
||||||
|
p := 0
|
||||||
if content.Type == gjson.String {
|
if content.Type == gjson.String {
|
||||||
// Assistant text -> single model content
|
node, _ = sjson.SetBytes(node, "parts.-1.text", content.String())
|
||||||
node := []byte(`{"role":"model","parts":[{"text":""}]}`)
|
|
||||||
node, _ = sjson.SetBytes(node, "parts.0.text", content.String())
|
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
} else if !content.Exists() || content.Type == gjson.Null {
|
p++
|
||||||
// Tool calls -> single model content with functionCall parts
|
}
|
||||||
tcs := m.Get("tool_calls")
|
|
||||||
if tcs.IsArray() {
|
|
||||||
node := []byte(`{"role":"model","parts":[]}`)
|
|
||||||
p := 0
|
|
||||||
fIDs := make([]string, 0)
|
|
||||||
for _, tc := range tcs.Array() {
|
|
||||||
if tc.Get("type").String() != "function" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fid := tc.Get("id").String()
|
|
||||||
fname := tc.Get("function.name").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.name", fname)
|
|
||||||
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
|
|
||||||
p++
|
|
||||||
if fid != "" {
|
|
||||||
fIDs = append(fIDs, fid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
|
||||||
|
|
||||||
// Append a single tool content combining name + response per function
|
// Tool calls -> single model content with functionCall parts
|
||||||
toolNode := []byte(`{"role":"user","parts":[]}`)
|
tcs := m.Get("tool_calls")
|
||||||
pp := 0
|
if tcs.IsArray() {
|
||||||
for _, fid := range fIDs {
|
fIDs := make([]string, 0)
|
||||||
if name, ok := tcID2Name[fid]; ok {
|
for _, tc := range tcs.Array() {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid)
|
if tc.Get("type").String() != "function" {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
continue
|
||||||
resp := toolResponses[fid]
|
}
|
||||||
if resp == "" {
|
fid := tc.Get("id").String()
|
||||||
resp = "{}"
|
fname := tc.Get("function.name").String()
|
||||||
}
|
fargs := tc.Get("function.arguments").String()
|
||||||
// Handle non-JSON output gracefully (matches dev branch approach)
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid)
|
||||||
if resp != "null" {
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
||||||
parsed := gjson.Parse(resp)
|
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
||||||
if parsed.Type == gjson.JSON {
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
|
||||||
toolNode, _ = sjson.SetRawBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(parsed.Raw))
|
p++
|
||||||
} else {
|
if fid != "" {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", resp)
|
fIDs = append(fIDs, fid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pp++
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
|
|
||||||
|
// Append a single tool content combining name + response per function
|
||||||
|
toolNode := []byte(`{"role":"user","parts":[]}`)
|
||||||
|
pp := 0
|
||||||
|
for _, fid := range fIDs {
|
||||||
|
if name, ok := tcID2Name[fid]; ok {
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid)
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
||||||
|
resp := toolResponses[fid]
|
||||||
|
if resp == "" {
|
||||||
|
resp = "{}"
|
||||||
}
|
}
|
||||||
|
// Handle non-JSON output gracefully (matches dev branch approach)
|
||||||
|
if resp != "null" {
|
||||||
|
parsed := gjson.Parse(resp)
|
||||||
|
if parsed.Type == gjson.JSON {
|
||||||
|
toolNode, _ = sjson.SetRawBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(parsed.Raw))
|
||||||
|
} else {
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pp++
|
||||||
}
|
}
|
||||||
if pp > 0 {
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", toolNode)
|
if pp > 0 {
|
||||||
}
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", toolNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,18 +360,3 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
|
|
||||||
// itoa converts int to string without strconv import for few usages.
|
// itoa converts int to string without strconv import for few usages.
|
||||||
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
||||||
|
|
||||||
// quoteIfNeeded ensures a string is valid JSON value (quotes plain text), pass-through for JSON objects/arrays.
|
|
||||||
func quoteIfNeeded(s string) string {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if s == "" {
|
|
||||||
return "\"\""
|
|
||||||
}
|
|
||||||
if len(s) > 0 && (s[0] == '{' || s[0] == '[') {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// escape quotes minimally
|
|
||||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
|
||||||
s = strings.ReplaceAll(s, "\"", "\\\"")
|
|
||||||
return "\"" + s + "\""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -205,52 +205,52 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
}
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
} else if role == "assistant" {
|
} else if role == "assistant" {
|
||||||
|
p := 0
|
||||||
|
node := []byte(`{"role":"model","parts":[]}`)
|
||||||
if content.Type == gjson.String {
|
if content.Type == gjson.String {
|
||||||
// Assistant text -> single model content
|
// Assistant text -> single model content
|
||||||
node := []byte(`{"role":"model","parts":[{"text":""}]}`)
|
node, _ = sjson.SetBytes(node, "parts.-1.text", content.String())
|
||||||
node, _ = sjson.SetBytes(node, "parts.0.text", content.String())
|
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
} else if !content.Exists() || content.Type == gjson.Null {
|
p++
|
||||||
// Tool calls -> single model content with functionCall parts
|
}
|
||||||
tcs := m.Get("tool_calls")
|
|
||||||
if tcs.IsArray() {
|
|
||||||
node := []byte(`{"role":"model","parts":[]}`)
|
|
||||||
p := 0
|
|
||||||
fIDs := make([]string, 0)
|
|
||||||
for _, tc := range tcs.Array() {
|
|
||||||
if tc.Get("type").String() != "function" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fid := tc.Get("id").String()
|
|
||||||
fname := tc.Get("function.name").String()
|
|
||||||
fargs := tc.Get("function.arguments").String()
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
|
||||||
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
|
|
||||||
p++
|
|
||||||
if fid != "" {
|
|
||||||
fIDs = append(fIDs, fid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
|
||||||
|
|
||||||
// Append a single tool content combining name + response per function
|
// Tool calls -> single model content with functionCall parts
|
||||||
toolNode := []byte(`{"role":"tool","parts":[]}`)
|
tcs := m.Get("tool_calls")
|
||||||
pp := 0
|
if tcs.IsArray() {
|
||||||
for _, fid := range fIDs {
|
fIDs := make([]string, 0)
|
||||||
if name, ok := tcID2Name[fid]; ok {
|
for _, tc := range tcs.Array() {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
if tc.Get("type").String() != "function" {
|
||||||
resp := toolResponses[fid]
|
continue
|
||||||
if resp == "" {
|
}
|
||||||
resp = "{}"
|
fid := tc.Get("id").String()
|
||||||
}
|
fname := tc.Get("function.name").String()
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp))
|
fargs := tc.Get("function.arguments").String()
|
||||||
pp++
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
||||||
|
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
|
||||||
|
p++
|
||||||
|
if fid != "" {
|
||||||
|
fIDs = append(fIDs, fid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", node)
|
||||||
|
|
||||||
|
// Append a single tool content combining name + response per function
|
||||||
|
toolNode := []byte(`{"role":"tool","parts":[]}`)
|
||||||
|
pp := 0
|
||||||
|
for _, fid := range fIDs {
|
||||||
|
if name, ok := tcID2Name[fid]; ok {
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
||||||
|
resp := toolResponses[fid]
|
||||||
|
if resp == "" {
|
||||||
|
resp = "{}"
|
||||||
}
|
}
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp))
|
||||||
|
pp++
|
||||||
}
|
}
|
||||||
if pp > 0 {
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "request.contents.-1", toolNode)
|
if pp > 0 {
|
||||||
}
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", toolNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,18 +334,3 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
|
|
||||||
// itoa converts int to string without strconv import for few usages.
|
// itoa converts int to string without strconv import for few usages.
|
||||||
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
||||||
|
|
||||||
// quoteIfNeeded ensures a string is valid JSON value (quotes plain text), pass-through for JSON objects/arrays.
|
|
||||||
func quoteIfNeeded(s string) string {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if s == "" {
|
|
||||||
return "\"\""
|
|
||||||
}
|
|
||||||
if len(s) > 0 && (s[0] == '{' || s[0] == '[') {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// escape quotes minimally
|
|
||||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
|
||||||
s = strings.ReplaceAll(s, "\"", "\\\"")
|
|
||||||
return "\"" + s + "\""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -207,15 +207,16 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
||||||
} else if role == "assistant" {
|
} else if role == "assistant" {
|
||||||
|
node := []byte(`{"role":"model","parts":[]}`)
|
||||||
|
p := 0
|
||||||
|
|
||||||
if content.Type == gjson.String {
|
if content.Type == gjson.String {
|
||||||
// Assistant text -> single model content
|
// Assistant text -> single model content
|
||||||
node := []byte(`{"role":"model","parts":[{"text":""}]}`)
|
node, _ = sjson.SetBytes(node, "parts.-1.text", content.String())
|
||||||
node, _ = sjson.SetBytes(node, "parts.0.text", content.String())
|
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
||||||
|
p++
|
||||||
} else if content.IsArray() {
|
} else if content.IsArray() {
|
||||||
// Assistant multimodal content (e.g. text + image) -> single model content with parts
|
// Assistant multimodal content (e.g. text + image) -> single model content with parts
|
||||||
node := []byte(`{"role":"model","parts":[]}`)
|
|
||||||
p := 0
|
|
||||||
for _, item := range content.Array() {
|
for _, item := range content.Array() {
|
||||||
switch item.Get("type").String() {
|
switch item.Get("type").String() {
|
||||||
case "text":
|
case "text":
|
||||||
@@ -237,47 +238,45 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
||||||
} else if !content.Exists() || content.Type == gjson.Null {
|
}
|
||||||
// Tool calls -> single model content with functionCall parts
|
|
||||||
tcs := m.Get("tool_calls")
|
|
||||||
if tcs.IsArray() {
|
|
||||||
node := []byte(`{"role":"model","parts":[]}`)
|
|
||||||
p := 0
|
|
||||||
fIDs := make([]string, 0)
|
|
||||||
for _, tc := range tcs.Array() {
|
|
||||||
if tc.Get("type").String() != "function" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fid := tc.Get("id").String()
|
|
||||||
fname := tc.Get("function.name").String()
|
|
||||||
fargs := tc.Get("function.arguments").String()
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
|
||||||
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
|
||||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature)
|
|
||||||
p++
|
|
||||||
if fid != "" {
|
|
||||||
fIDs = append(fIDs, fid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
|
||||||
|
|
||||||
// Append a single tool content combining name + response per function
|
// Tool calls -> single model content with functionCall parts
|
||||||
toolNode := []byte(`{"role":"tool","parts":[]}`)
|
tcs := m.Get("tool_calls")
|
||||||
pp := 0
|
if tcs.IsArray() {
|
||||||
for _, fid := range fIDs {
|
fIDs := make([]string, 0)
|
||||||
if name, ok := tcID2Name[fid]; ok {
|
for _, tc := range tcs.Array() {
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
if tc.Get("type").String() != "function" {
|
||||||
resp := toolResponses[fid]
|
continue
|
||||||
if resp == "" {
|
}
|
||||||
resp = "{}"
|
fid := tc.Get("id").String()
|
||||||
}
|
fname := tc.Get("function.name").String()
|
||||||
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp))
|
fargs := tc.Get("function.arguments").String()
|
||||||
pp++
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
|
||||||
|
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature)
|
||||||
|
p++
|
||||||
|
if fid != "" {
|
||||||
|
fIDs = append(fIDs, fid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
||||||
|
|
||||||
|
// Append a single tool content combining name + response per function
|
||||||
|
toolNode := []byte(`{"role":"tool","parts":[]}`)
|
||||||
|
pp := 0
|
||||||
|
for _, fid := range fIDs {
|
||||||
|
if name, ok := tcID2Name[fid]; ok {
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name)
|
||||||
|
resp := toolResponses[fid]
|
||||||
|
if resp == "" {
|
||||||
|
resp = "{}"
|
||||||
}
|
}
|
||||||
|
toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp))
|
||||||
|
pp++
|
||||||
}
|
}
|
||||||
if pp > 0 {
|
}
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", toolNode)
|
if pp > 0 {
|
||||||
}
|
out, _ = sjson.SetRawBytes(out, "contents.-1", toolNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,18 +362,3 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
|
|
||||||
// itoa converts int to string without strconv import for few usages.
|
// itoa converts int to string without strconv import for few usages.
|
||||||
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
func itoa(i int) string { return fmt.Sprintf("%d", i) }
|
||||||
|
|
||||||
// quoteIfNeeded ensures a string is valid JSON value (quotes plain text), pass-through for JSON objects/arrays.
|
|
||||||
func quoteIfNeeded(s string) string {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if s == "" {
|
|
||||||
return "\"\""
|
|
||||||
}
|
|
||||||
if len(s) > 0 && (s[0] == '{' || s[0] == '[') {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// escape quotes minimally
|
|
||||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
|
||||||
s = strings.ReplaceAll(s, "\"", "\\\"")
|
|
||||||
return "\"" + s + "\""
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user