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,17 +222,17 @@ 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 // Tool calls -> single model content with functionCall parts
tcs := m.Get("tool_calls") tcs := m.Get("tool_calls")
if tcs.IsArray() { if tcs.IsArray() {
node := []byte(`{"role":"model","parts":[]}`)
p := 0
fIDs := make([]string, 0) fIDs := make([]string, 0)
for _, tc := range tcs.Array() { for _, tc := range tcs.Array() {
if tc.Get("type").String() != "function" { if tc.Get("type").String() != "function" {
@@ -282,7 +282,6 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
} }
} }
} }
}
// tools -> request.tools[0].functionDeclarations + request.tools[0].googleSearch passthrough // tools -> request.tools[0].functionDeclarations + request.tools[0].googleSearch passthrough
tools := gjson.GetBytes(rawJSON, "tools") tools := gjson.GetBytes(rawJSON, "tools")
@@ -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,17 +205,18 @@ 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 // Tool calls -> single model content with functionCall parts
tcs := m.Get("tool_calls") tcs := m.Get("tool_calls")
if tcs.IsArray() { if tcs.IsArray() {
node := []byte(`{"role":"model","parts":[]}`)
p := 0
fIDs := make([]string, 0) fIDs := make([]string, 0)
for _, tc := range tcs.Array() { for _, tc := range tcs.Array() {
if tc.Get("type").String() != "function" { if tc.Get("type").String() != "function" {
@@ -255,7 +256,6 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
} }
} }
} }
}
// tools -> request.tools[0].functionDeclarations + request.tools[0].googleSearch passthrough // tools -> request.tools[0].functionDeclarations + request.tools[0].googleSearch passthrough
tools := gjson.GetBytes(rawJSON, "tools") tools := gjson.GetBytes(rawJSON, "tools")
@@ -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" {
if content.Type == gjson.String {
// Assistant text -> single model content
node := []byte(`{"role":"model","parts":[{"text":""}]}`)
node, _ = sjson.SetBytes(node, "parts.0.text", content.String())
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
} else if content.IsArray() {
// Assistant multimodal content (e.g. text + image) -> single model content with parts
node := []byte(`{"role":"model","parts":[]}`) node := []byte(`{"role":"model","parts":[]}`)
p := 0 p := 0
if content.Type == gjson.String {
// Assistant text -> single model content
node, _ = sjson.SetBytes(node, "parts.-1.text", content.String())
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
p++
} else if content.IsArray() {
// Assistant multimodal content (e.g. text + image) -> single model content with parts
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,12 +238,11 @@ 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 // Tool calls -> single model content with functionCall parts
tcs := m.Get("tool_calls") tcs := m.Get("tool_calls")
if tcs.IsArray() { if tcs.IsArray() {
node := []byte(`{"role":"model","parts":[]}`)
p := 0
fIDs := make([]string, 0) fIDs := make([]string, 0)
for _, tc := range tcs.Array() { for _, tc := range tcs.Array() {
if tc.Get("type").String() != "function" { if tc.Get("type").String() != "function" {
@@ -282,7 +282,6 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
} }
} }
} }
}
// tools -> tools[0].functionDeclarations + tools[0].googleSearch passthrough // tools -> tools[0].functionDeclarations + tools[0].googleSearch passthrough
tools := gjson.GetBytes(rawJSON, "tools") tools := gjson.GetBytes(rawJSON, "tools")
@@ -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 + "\""
}