fix(translator): standardize content node handling across translators for assistant and tool calls
This commit is contained in:
Luis Pater
2025-12-17 13:16:07 +08:00
parent 6586f08584
commit ffdfad8482
3 changed files with 131 additions and 178 deletions

View File

@@ -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 + "\""
}

View File

@@ -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 + "\""
}

View File

@@ -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 + "\""
}