perf(translator): optimize array allocation logic and replace JoinRawArray with SetRawArrayItems methods

This commit is contained in:
Luis Pater
2026-07-19 15:01:37 +08:00
parent 3c2010bcf3
commit 7c61e982e4
28 changed files with 175 additions and 82 deletions

View File

@@ -342,7 +342,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
}
// contents
contentItems := make([][]byte, 0, 16)
contentItems := translatorcommon.NewRawArrayItems(gjson.GetBytes(rawJSON, "messages.#").Int())
// tool_use_id → tool_name lookup, populated incrementally during the main loop.
// Claude's tool_result references tool_use by ID; Gemini requires functionResponse.name.
@@ -629,7 +629,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
allowedToolKeys := []string{"name", "description", "behavior", "parameters", "parametersJsonSchema", "response", "responseJsonSchema"}
toolsResult := gjson.GetBytes(rawJSON, "tools")
if toolsResult.IsArray() {
functionDeclarations := make([][]byte, 0, 8)
var functionDeclarations [][]byte
toolsResults := toolsResult.Array()
for i := 0; i < len(toolsResults); i++ {
toolResult := toolsResults[i]
@@ -686,7 +686,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
out, _ = sjson.SetRawBytes(out, "request.systemInstruction", antigravityClaudeContent("user", systemParts))
}
if len(contentItems) > 0 {
out, _ = sjson.SetRawBytes(out, "request.contents", translatorcommon.JoinRawArray(contentItems))
out = translatorcommon.SetRawArrayItems(out, "request.contents", contentItems)
}
if toolDeclCount > 0 {
out, _ = sjson.SetRawBytes(out, "request.tools", toolsJSON)

View File

@@ -60,7 +60,7 @@ func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _
// Normalize roles in request.contents: default to valid values if missing/invalid
contents := gjson.GetBytes(rawJSON, "request.contents")
if contents.IsArray() {
contentItems := make([][]byte, 0, 16)
contentItems := translatorcommon.NewRawArrayItems(contents.Get("#").Int())
rolesChanged := false
previousRole := ""
contents.ForEach(func(_, value gjson.Result) bool {
@@ -87,7 +87,7 @@ func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _
toolsResult := gjson.GetBytes(rawJSON, "request.tools")
if toolsResult.IsArray() {
seenFunctionNames := make(map[string]struct{})
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
toolsResult.ForEach(func(toolIndex, tool gjson.Result) bool {
toolJSON := []byte(tool.Raw)
for _, key := range []string{"functionDeclarations", "function_declarations"} {
@@ -96,7 +96,7 @@ func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _
continue
}
declarationItems := make([][]byte, 0, 8)
var declarationItems [][]byte
declarations.ForEach(func(_, declaration gjson.Result) bool {
mappedName := util.MapSanitizedFunctionName(functionNameMap, declaration.Get("name").String())
if mappedName != "" {
@@ -140,7 +140,7 @@ func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _
func removeEmptyGeminiFunctionTools(rawJSON []byte) []byte {
tools := gjson.GetBytes(rawJSON, "request.tools")
cleanedTools := make([][]byte, 0, 8)
var cleanedTools [][]byte
for _, tool := range tools.Array() {
toolJSON := []byte(tool.Raw)
if tool.IsObject() {
@@ -178,7 +178,7 @@ func rewriteGeminiFunctionNames(rawJSON []byte, functionNameMap map[string]strin
}
if canBatchContents {
contentsChanged := false
contentItems := make([][]byte, 0, 16)
contentItems := translatorcommon.NewRawArrayItems(contents.Get("#").Int())
contents.ForEach(func(_, content gjson.Result) bool {
contentJSON := []byte(content.Raw)
partsChanged := false
@@ -512,7 +512,7 @@ func fixCLIToolResponse(input string) (string, error) {
}
// Initialize data structures for processing and grouping
contentItems := make([][]byte, 0, 16)
contentItems := translatorcommon.NewRawArrayItems(contents.Get("#").Int())
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
var collectedResponses []gjson.Result // Standalone responses to be matched
appendFunctionResponses := func(responses []gjson.Result, callNames []string) {

View File

@@ -22,9 +22,9 @@ func ConvertInteractionsRequestToAntigravity(modelName string, inputRawJSON []by
}
out = copyInteractionsSystemToAntigravity(out, root)
out = copyInteractionsGenerationConfigToAntigravity(out, root)
contentItems := make([][]byte, 0, 16)
contentItems := translatorcommon.NewRawArrayItems(root.Get("input.#").Int())
appendInteractionsInputToAntigravity(&contentItems, root.Get("input"))
out, _ = sjson.SetRawBytes(out, "request.contents", translatorcommon.JoinRawArray(contentItems))
out = translatorcommon.SetRawArrayItems(out, "request.contents", contentItems)
out = copyInteractionsToolsToAntigravity(out, root, functionNameMap)
out = rewriteInteractionsFunctionNames(out, functionNameMap)
out = attachDefaultAntigravitySafetySettings(out)
@@ -46,7 +46,7 @@ func rewriteInteractionsFunctionNames(out []byte, functionNameMap map[string]str
}
if canBatchContents {
contentsChanged := false
contentItems := make([][]byte, 0, 16)
contentItems := translatorcommon.NewRawArrayItems(contents.Get("#").Int())
contents.ForEach(func(_, content gjson.Result) bool {
contentJSON := []byte(content.Raw)
partsChanged := false
@@ -507,8 +507,8 @@ func copyInteractionsToolsToAntigravity(out []byte, root gjson.Result, functionN
out, _ = sjson.SetRawBytes(out, "request.tools", []byte(tools.Raw))
return out
}
functionDeclarations := make([][]byte, 0, 8)
otherTools := make([][]byte, 0)
var functionDeclarations [][]byte
var otherTools [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if decls := tool.Get("functionDeclarations"); decls.Exists() && decls.IsArray() {
decls.ForEach(func(_, decl gjson.Result) bool {

View File

@@ -302,7 +302,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
if len(systemParts) > 0 {
out, _ = sjson.SetRawBytes(out, "request.systemInstruction", antigravityOpenAIContent("user", systemParts))
}
out, _ = sjson.SetRawBytes(out, "request.contents", translatorcommon.JoinRawArray(contentItems))
out = translatorcommon.SetRawArrayItems(out, "request.contents", contentItems)
}
// tools -> request.tools[].functionDeclarations + request.tools[].googleSearch/codeExecution/urlContext passthrough

View File

@@ -67,7 +67,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
out := []byte(fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID))
root := gjson.ParseBytes(rawJSON)
messageItems := make([][]byte, 0, 16)
messageItems := translatorcommon.NewRawArrayItems(root.Get("contents.#").Int())
// Helper for generating tool call IDs in the form: toolu_<alphanum>
// This ensures unique identifiers for tool calls in the Claude Code format
@@ -359,7 +359,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
return true
})
}
out, _ = sjson.SetRawBytes(out, "messages", translatorcommon.JoinRawArray(messageItems))
out = translatorcommon.SetRawArrayItems(out, "messages", messageItems)
// Tools mapping: Gemini functionDeclarations -> Claude Code tools
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {

View File

@@ -20,9 +20,9 @@ func ConvertInteractionsRequestToClaude(modelName string, inputRawJSON []byte, s
}
out = copyInteractionsSystemToClaude(out, root)
out = copyInteractionsGenerationConfigToClaude(out, root)
messageItems := make([][]byte, 0, 16)
messageItems := translatorcommon.NewRawArrayItems(root.Get("input.#").Int())
appendInteractionsInputToClaudeMessages(&messageItems, root.Get("input"))
out, _ = sjson.SetRawBytes(out, "messages", translatorcommon.JoinRawArray(messageItems))
out = translatorcommon.SetRawArrayItems(out, "messages", messageItems)
out = copyInteractionsToolsToClaude(out, root)
return out
}
@@ -297,7 +297,7 @@ func copyInteractionsToolsToClaude(out []byte, root gjson.Result) []byte {
if !tools.Exists() || !tools.IsArray() {
return out
}
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if tool.Get("function_declarations").IsArray() {
tool.Get("function_declarations").ForEach(func(_, decl gjson.Result) bool {

View File

@@ -289,13 +289,13 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
out, _ = sjson.SetRawBytes(out, "system", common.JoinRawArray(systemBlocks))
}
if len(messageBlocks) > 0 {
out, _ = sjson.SetRawBytes(out, "messages", common.JoinRawArray(messageBlocks))
out = common.SetRawArrayItems(out, "messages", messageBlocks)
}
}
// Tools mapping: OpenAI tools -> Claude Code tools
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 {
anthropicTools := make([][]byte, 0, 8)
var anthropicTools [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if tool.Get("type").String() == "function" {
function := tool.Get("function")

View File

@@ -128,7 +128,11 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
out, _ = sjson.SetBytes(out, "stream", stream)
// instructions -> as a leading message (use role user for Claude API compatibility)
messageBlocks := make([][]byte, 0, 16)
messageCapacity := root.Get("input.#").Int()
if instructions := root.Get("instructions"); instructions.Type == gjson.String && instructions.String() != "" {
messageCapacity++
}
messageBlocks := common.NewRawArrayItems(messageCapacity)
instructionsText := ""
extractedFromSystem := false
if instr := root.Get("instructions"); instr.Exists() && instr.Type == gjson.String {
@@ -419,14 +423,14 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
}
flushPendingReasoning()
flushPendingToolUses()
out, _ = sjson.SetRawBytes(out, "messages", common.JoinRawArray(messageBlocks))
out = common.SetRawArrayItems(out, "messages", messageBlocks)
includedToolNames := map[string]struct{}{}
toolNameMap := map[string]string{}
// tools mapping: parameters -> input_schema
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
convertedTools := convertResponsesToolToClaudeTools(tool, toolNameMap)
for _, tJSON := range convertedTools {

View File

@@ -46,7 +46,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
rootResult := gjson.ParseBytes(rawJSON)
toolNameMap := buildReverseMapFromClaudeOriginalToShort(rawJSON)
template, _ = sjson.SetBytes(template, "model", modelName)
inputItems := make([][]byte, 0, 16)
inputItems := translatorcommon.NewRawArrayItems(rootResult.Get("messages.#").Int())
// Process system messages and convert them to input content format.
systemsResult := rootResult.Get("system")
@@ -347,7 +347,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
if toolsResult.IsArray() {
template, _ = sjson.SetRawBytes(template, "tools", translatorcommon.JoinRawArray(toolItems))
}
template, _ = sjson.SetRawBytes(template, "input", translatorcommon.JoinRawArray(inputItems))
template = translatorcommon.SetRawArrayItems(template, "input", inputItems)
return template
}

View File

@@ -42,7 +42,7 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
out := []byte(`{"model":"","instructions":"","input":[]}`)
root := gjson.ParseBytes(rawJSON)
inputItems := make([][]byte, 0, 16)
inputItems := translatorcommon.NewRawArrayItems(root.Get("contents.#").Int())
// Pre-compute tool name shortening map from declared functionDeclarations
shortMap := map[string]string{}
@@ -230,12 +230,12 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
}
}
out, _ = sjson.SetRawBytes(out, "input", translatorcommon.JoinRawArray(inputItems))
out = translatorcommon.SetRawArrayItems(out, "input", inputItems)
// Tools mapping: Gemini functionDeclarations -> Codex tools
tools := root.Get("tools")
if tools.IsArray() {
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
out, _ = sjson.SetBytes(out, "tool_choice", "auto")
tarr := tools.Array()
for i := 0; i < len(tarr); i++ {

View File

@@ -20,9 +20,9 @@ func ConvertInteractionsRequestToCodex(modelName string, inputRawJSON []byte, st
}
out = copyInteractionsSystemToCodex(out, root)
out = copyInteractionsGenerationConfigToCodex(out, root)
inputItems := make([][]byte, 0, 16)
inputItems := translatorcommon.NewRawArrayItems(root.Get("input.#").Int())
appendInteractionsInputToCodex(&inputItems, root.Get("input"))
out, _ = sjson.SetRawBytes(out, "input", translatorcommon.JoinRawArray(inputItems))
out = translatorcommon.SetRawArrayItems(out, "input", inputItems)
out = copyInteractionsToolsToCodex(out, root)
out = copyInteractionsCodexTopLevel(out, root)
return out

View File

@@ -122,7 +122,7 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
// Build input from messages, handling all message types including tool calls
out, _ = sjson.SetRawBytes(out, "input", []byte(`[]`))
inputItems := make([][]byte, 0, 16)
inputItems := translatorcommon.NewRawArrayItems(messages.Get("#").Int())
if messages.IsArray() {
arr := messages.Array()
for i := 0; i < len(arr); i++ {
@@ -343,7 +343,7 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
}
}
}
out, _ = sjson.SetRawBytes(out, "input", translatorcommon.JoinRawArray(inputItems))
out = translatorcommon.SetRawArrayItems(out, "input", inputItems)
// Map response_format and text settings to Responses API text.format
rf := gjson.GetBytes(rawJSON, "response_format")

View File

@@ -121,7 +121,7 @@ func normalizeCodexBuiltinToolArray(rawJSON []byte, path string) []byte {
}
changed := false
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
item := []byte(tool.Raw)
currentType := tool.Get("type").String()

View File

@@ -2,6 +2,9 @@ package common
import (
"strconv"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
func GeminiTokenCountJSON(count int64) []byte {
@@ -22,6 +25,14 @@ func ClaudeInputTokensJSON(count int64) []byte {
return out
}
// NewRawArrayItems creates a raw item slice sized for the expected input.
func NewRawArrayItems(capacity int64) [][]byte {
if capacity <= 0 {
return nil
}
return make([][]byte, 0, int(capacity))
}
func JoinRawArray(items [][]byte) []byte {
if len(items) == 0 {
return []byte("[]")
@@ -41,6 +52,27 @@ func JoinRawArray(items [][]byte) []byte {
return append(out, ']')
}
// SetRawArrayItems replaces an empty JSON array at path with raw items.
// The single-item path avoids allocating an intermediate joined array.
func SetRawArrayItems(data []byte, path string, items [][]byte) []byte {
if len(items) == 0 {
return data
}
if len(items) == 1 {
array := gjson.GetBytes(data, path)
if array.Raw == "[]" && array.Index >= 0 && array.Index+len(array.Raw) <= len(data) {
out := make([]byte, 0, len(data)+len(items[0]))
out = append(out, data[:array.Index]...)
out = append(out, '[')
out = append(out, items[0]...)
out = append(out, ']')
return append(out, data[array.Index+len(array.Raw):]...)
}
}
data, _ = sjson.SetRawBytes(data, path, JoinRawArray(items))
return data
}
func SSEEventData(event string, payload []byte) []byte {
out := make([]byte, 0, len(event)+len(payload)+14)
out = append(out, "event: "...)

View File

@@ -21,3 +21,36 @@ func TestJoinRawArray(t *testing.T) {
})
}
}
func TestNewRawArrayItems(t *testing.T) {
if items := NewRawArrayItems(0); items != nil {
t.Fatalf("NewRawArrayItems(0) = %#v, want nil", items)
}
if items := NewRawArrayItems(3); len(items) != 0 || cap(items) != 3 {
t.Fatalf("NewRawArrayItems(3) len = %d, cap = %d; want len 0, cap 3", len(items), cap(items))
}
}
func TestSetRawArrayItems(t *testing.T) {
tests := []struct {
name string
data string
path string
items [][]byte
want string
}{
{name: "empty", data: `{"items":[]}`, path: "items", want: `{"items":[]}`},
{name: "single nested", data: `{"before":1,"request":{"contents":[]},"after":2}`, path: "request.contents", items: [][]byte{[]byte(`{"id":1}`)}, want: `{"before":1,"request":{"contents":[{"id":1}]},"after":2}`},
{name: "single fallback", data: `{"items":[{"old":1},{"old":2}]}`, path: "items", items: [][]byte{[]byte(`{"id":1}`)}, want: `{"items":[{"id":1}]}`},
{name: "multiple", data: `{"items":[]}`, path: "items", items: [][]byte{[]byte(`{"id":1}`), []byte(`{"id":2}`)}, want: `{"items":[{"id":1},{"id":2}]}`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := SetRawArrayItems([]byte(test.data), test.path, test.items)
if string(got) != test.want {
t.Fatalf("SetRawArrayItems() = %s, want %s", got, test.want)
}
})
}
}

View File

@@ -63,7 +63,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
// contents
if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() {
contentItems := make([][]byte, 0, 16)
contentItems := translatorcommon.NewRawArrayItems(messagesResult.Get("#").Int())
messagesResult.ForEach(func(_, messageResult gjson.Result) bool {
roleResult := messageResult.Get("role")
if roleResult.Type != gjson.String {
@@ -187,12 +187,12 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
}
}
}
out, _ = sjson.SetRawBytes(out, "contents", translatorcommon.JoinRawArray(contentItems))
out = translatorcommon.SetRawArrayItems(out, "contents", contentItems)
}
// tools
if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() {
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
inputSchemaResult := toolResult.Get("input_schema")
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {

View File

@@ -31,7 +31,7 @@ func ConvertGeminiRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte
toolsResult := gjson.GetBytes(rawJSON, "tools")
if toolsResult.Exists() && toolsResult.IsArray() {
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
toolsChanged := false
toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
tool := []byte(toolResult.Raw)
@@ -44,7 +44,7 @@ func ConvertGeminiRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte
declarations := gjson.GetBytes(tool, "function_declarations")
if declarations.IsArray() {
declarationItems := make([][]byte, 0, 8)
var declarationItems [][]byte
declarationsChanged := false
declarations.ForEach(func(_, declarationResult gjson.Result) bool {
declaration := []byte(declarationResult.Raw)
@@ -86,7 +86,7 @@ func ConvertGeminiRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte
})
if rolesChanged {
prevRole = ""
contentItems := make([][]byte, 0, 16)
contentItems := translatorcommon.NewRawArrayItems(contents.Get("#").Int())
contents.ForEach(func(_, value gjson.Result) bool {
role := value.Get("role").String()
item := []byte(value.Raw)
@@ -161,7 +161,7 @@ func backfillEmptyFunctionResponseNames(data []byte) []byte {
}
changed := false
contentItems := make([][]byte, 0, 16)
contentItems := translatorcommon.NewRawArrayItems(contents.Get("#").Int())
var pendingCallNames []string
contents.ForEach(func(contentIdx, content gjson.Result) bool {

View File

@@ -39,9 +39,9 @@ func ConvertInteractionsRequestToGemini(modelName string, inputRawJSON []byte, s
out = copyInteractionsTools(out, root)
out = copyInteractionsToolChoice(out, root)
out = copyInteractionsServiceTier(out, root)
contentItems := make([][]byte, 0, 16)
contentItems := translatorcommon.NewRawArrayItems(root.Get("input.#").Int())
appendInteractionsInput(&contentItems, root.Get("input"))
out, _ = sjson.SetRawBytes(out, "contents", translatorcommon.JoinRawArray(contentItems))
out = translatorcommon.SetRawArrayItems(out, "contents", contentItems)
return out
}
@@ -56,7 +56,7 @@ func ConvertGeminiRequestToInteractions(modelName string, inputRawJSON []byte, s
out = normalizeGeminiThinkingConfigForInteractions(out)
}
out = copyGeminiToolsToInteractions(out, root)
inputItems := make([][]byte, 0, 16)
inputItems := translatorcommon.NewRawArrayItems(root.Get("contents.#").Int())
root.Get("contents").ForEach(func(_, content gjson.Result) bool {
role := content.Get("role").String()
stepType := "user_input"
@@ -88,13 +88,13 @@ func ConvertGeminiRequestToInteractions(modelName string, inputRawJSON []byte, s
}
step := []byte(`{"type":"","content":[]}`)
step, _ = sjson.SetBytes(step, "type", currentStepType)
step, _ = sjson.SetRawBytes(step, "content", translatorcommon.JoinRawArray([][]byte{item}))
step = translatorcommon.SetRawArrayItems(step, "content", [][]byte{item})
inputItems = append(inputItems, step)
return true
})
return true
})
out, _ = sjson.SetRawBytes(out, "input", translatorcommon.JoinRawArray(inputItems))
out = translatorcommon.SetRawArrayItems(out, "input", inputItems)
out, _ = sjson.SetBytes(out, "stream", stream)
return out
}

View File

@@ -303,7 +303,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
if len(contentItems) > 0 && gjson.GetBytes(contentItems[len(contentItems)-1], "role").String() == "model" {
contentItems = contentItems[:len(contentItems)-1]
}
out, _ = sjson.SetRawBytes(out, "contents", translatorcommon.JoinRawArray(contentItems))
out = translatorcommon.SetRawArrayItems(out, "contents", contentItems)
}
// tools -> tools[].functionDeclarations + tools[].googleSearch/codeExecution/urlContext passthrough

View File

@@ -359,12 +359,12 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
if len(contentItems) > 0 && shouldStripTrailingOpenAIResponsesModelPrefill(gjson.ParseBytes(contentItems[len(contentItems)-1])) {
contentItems = contentItems[:len(contentItems)-1]
}
out, _ = sjson.SetRawBytes(out, "contents", translatorcommon.JoinRawArray(contentItems))
out = translatorcommon.SetRawArrayItems(out, "contents", contentItems)
} else if input.Exists() && input.Type == gjson.String {
// Simple string input conversion to user message.
part := []byte(`{"text":""}`)
part, _ = sjson.SetBytes(part, "text", input.String())
out, _ = sjson.SetRawBytes(out, "contents", translatorcommon.JoinRawArray([][]byte{geminiContent("user", [][]byte{part})}))
out = translatorcommon.SetRawArrayItems(out, "contents", [][]byte{geminiContent("user", [][]byte{part})})
}
if len(systemParts) > 0 {
out, _ = sjson.SetRawBytes(out, "systemInstruction", geminiSystemInstruction(systemParts))
@@ -372,7 +372,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
// Convert tools to Gemini functionDeclarations format
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
functionDeclarations := make([][]byte, 0, 8)
var functionDeclarations [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if tool.Get("type").String() == "function" {
funcDecl := []byte(`{"name":"","description":"","parametersJsonSchema":{}}`)

View File

@@ -116,12 +116,12 @@ func appendClaudeMessagesToInteractions(out []byte, messages gjson.Result) []byt
if !messages.Exists() || !messages.IsArray() {
return out
}
inputItems := make([][]byte, 0, 16)
inputItems := translatorcommon.NewRawArrayItems(messages.Get("#").Int())
messages.ForEach(func(_, message gjson.Result) bool {
appendClaudeMessageToInteractions(&inputItems, message)
return true
})
out, _ = sjson.SetRawBytes(out, "input", translatorcommon.JoinRawArray(inputItems))
out = translatorcommon.SetRawArrayItems(out, "input", inputItems)
return out
}
@@ -247,7 +247,7 @@ func copyClaudeToolsToInteractions(out []byte, root gjson.Result) []byte {
if !tools.Exists() || !tools.IsArray() {
return out
}
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
name := strings.TrimSpace(tool.Get("name").String())
if name == "" {

View File

@@ -99,7 +99,11 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
}
// Process messages and system.
messageItems := make([][]byte, 0, 16)
messageCapacity := root.Get("messages.#").Int()
if root.Get("system").Exists() {
messageCapacity++
}
messageItems := translatorcommon.NewRawArrayItems(messageCapacity)
// Handle system message first.
systemContentItems := make([][]byte, 0, 2)
@@ -285,12 +289,12 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
// Set messages.
if len(messageItems) > 0 {
out, _ = sjson.SetRawBytes(out, "messages", translatorcommon.JoinRawArray(messageItems))
out = translatorcommon.SetRawArrayItems(out, "messages", messageItems)
}
// Process tools - convert Anthropic tools to OpenAI functions
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
openAIToolJSON := []byte(`{"type":"function","function":{"name":"","description":""}}`)
openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.name", tool.Get("name").String())

View File

@@ -134,7 +134,11 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
}
// Process contents (Gemini messages) -> OpenAI messages
messageItems := make([][]byte, 0, 16)
messageCapacity := root.Get("contents.#").Int()
if root.Get("systemInstruction").Exists() || root.Get("system_instruction").Exists() {
messageCapacity++
}
messageItems := translatorcommon.NewRawArrayItems(messageCapacity)
var toolCallIDs []string // Track tool call IDs for matching with tool results
toolCallConsumeIdx := 0
@@ -288,11 +292,11 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
return true
})
}
out, _ = sjson.SetRawBytes(out, "messages", translatorcommon.JoinRawArray(messageItems))
out = translatorcommon.SetRawArrayItems(out, "messages", messageItems)
// Tools mapping: Gemini tools -> OpenAI tools
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {
functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {

View File

@@ -16,10 +16,14 @@ func ConvertInteractionsRequestToOpenAI(modelName string, inputRawJSON []byte, s
if stream || root.Get("stream").Bool() {
out, _ = sjson.SetBytes(out, "stream", true)
}
messageItems := make([][]byte, 0, 16)
messageCapacity := root.Get("input.#").Int()
if interactionsText(root.Get("system_instruction")) != "" {
messageCapacity++
}
messageItems := translatorcommon.NewRawArrayItems(messageCapacity)
appendInteractionsSystemToOpenAI(&messageItems, root)
appendInteractionsInputToOpenAIMessages(&messageItems, root.Get("input"))
out, _ = sjson.SetRawBytes(out, "messages", translatorcommon.JoinRawArray(messageItems))
out = translatorcommon.SetRawArrayItems(out, "messages", messageItems)
out = copyInteractionsToolsToOpenAI(out, root)
out = copyInteractionsGenerationConfigToOpenAI(out, root)
out = copyInteractionsOpenAITopLevel(out, root)
@@ -143,7 +147,7 @@ func appendInteractionsFunctionCallToOpenAI(items *[][]byte, step gjson.Result)
toolCall, _ = sjson.SetBytes(toolCall, "id", callID)
toolCall, _ = sjson.SetBytes(toolCall, "function.name", step.Get("name").String())
toolCall, _ = sjson.SetBytes(toolCall, "function.arguments", jsonStringValue(step.Get("arguments"), "{}"))
msg, _ = sjson.SetRawBytes(msg, "tool_calls", translatorcommon.JoinRawArray([][]byte{toolCall}))
msg = translatorcommon.SetRawArrayItems(msg, "tool_calls", [][]byte{toolCall})
*items = append(*items, msg)
}
@@ -159,7 +163,7 @@ func copyInteractionsToolsToOpenAI(out []byte, root gjson.Result) []byte {
if !tools.Exists() || !tools.IsArray() {
return out
}
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if converted, ok := openAIToolFromInteractionsTool(tool); ok {
toolItems = append(toolItems, converted)

View File

@@ -35,7 +35,7 @@ func appendOpenAIMessagesToInteractions(out []byte, messages gjson.Result) []byt
if !messages.Exists() || !messages.IsArray() {
return out
}
inputItems := make([][]byte, 0, 16)
inputItems := translatorcommon.NewRawArrayItems(messages.Get("#").Int())
var systemBuilder strings.Builder
messages.ForEach(func(_, message gjson.Result) bool {
role := strings.ToLower(strings.TrimSpace(message.Get("role").String()))
@@ -55,7 +55,7 @@ func appendOpenAIMessagesToInteractions(out []byte, messages gjson.Result) []byt
if systemBuilder.Len() > 0 {
out, _ = sjson.SetBytes(out, "system_instruction", systemBuilder.String())
}
out, _ = sjson.SetRawBytes(out, "input", translatorcommon.JoinRawArray(inputItems))
out = translatorcommon.SetRawArrayItems(out, "input", inputItems)
return out
}
@@ -233,7 +233,7 @@ func appendOpenAIChatToolsToInteractions(out []byte, tools gjson.Result) []byte
if !tools.Exists() || !tools.IsArray() {
return out
}
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if converted, ok := openAIChatToolToInteractions(tool); ok {
toolItems = append(toolItems, converted)

View File

@@ -227,7 +227,7 @@ func responsesInputItemToInteractions(item gjson.Result, functionNamesByCallID m
}
func appendResponsesContentToInteractions(step []byte, content gjson.Result) []byte {
contentItems := make([][]byte, 0, 4)
var contentItems [][]byte
if content.Type == gjson.String {
part := []byte(`{"type":"text","text":""}`)
part, _ = sjson.SetBytes(part, "text", content.String())
@@ -245,7 +245,7 @@ func appendResponsesContentToInteractions(step []byte, content gjson.Result) []b
}
}
if len(contentItems) > 0 {
step, _ = sjson.SetRawBytes(step, "content", translatorcommon.JoinRawArray(contentItems))
step = translatorcommon.SetRawArrayItems(step, "content", contentItems)
}
return step
}
@@ -328,7 +328,7 @@ func appendResponsesToolsToInteractions(out []byte, tools gjson.Result) []byte {
if !tools.Exists() || !tools.IsArray() {
return out
}
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
switch tool.Get("type").String() {
case "function", "":
@@ -434,7 +434,7 @@ func interactionsInputItemToResponses(item gjson.Result) []byte {
}
func interactionsMessageToResponses(item gjson.Result, role string) []byte {
contentItems := make([][]byte, 0, 4)
var contentItems [][]byte
content := item.Get("content")
if content.Type == gjson.String {
partType := "input_text"
@@ -455,19 +455,19 @@ func interactionsMessageToResponses(item gjson.Result, role string) []byte {
}
out := []byte(`{"type":"message","role":"","content":[]}`)
out, _ = sjson.SetBytes(out, "role", role)
out, _ = sjson.SetRawBytes(out, "content", translatorcommon.JoinRawArray(contentItems))
out = translatorcommon.SetRawArrayItems(out, "content", contentItems)
return out
}
func interactionsThoughtToResponses(item gjson.Result) []byte {
summaryItems := make([][]byte, 0, 2)
var summaryItems [][]byte
for _, text := range interactionsContentTexts(item.Get("content")) {
part := []byte(`{"type":"summary_text","text":""}`)
part, _ = sjson.SetBytes(part, "text", text)
summaryItems = append(summaryItems, part)
}
out := []byte(`{"type":"reasoning","summary":[]}`)
out, _ = sjson.SetRawBytes(out, "summary", translatorcommon.JoinRawArray(summaryItems))
out = translatorcommon.SetRawArrayItems(out, "summary", summaryItems)
return out
}
@@ -551,7 +551,7 @@ func appendInteractionsToolsToResponses(out []byte, tools gjson.Result) []byte {
if !tools.Exists() || !tools.IsArray() {
return out
}
toolItems := make([][]byte, 0, 8)
var toolItems [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if converted, ok := responsesToolFromInteractionsTool(tool); ok {
toolItems = append(toolItems, converted)

View File

@@ -163,7 +163,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
message, _ = sjson.SetBytes(message, "role", role)
if content := item.Get("content"); content.Exists() && content.IsArray() {
contentItems := make([][]byte, 0, 4)
var contentItems [][]byte
content.ForEach(func(_, contentItem gjson.Result) bool {
contentType := contentItem.Get("type").String()
if contentType == "" {
@@ -187,7 +187,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
}
return true
})
message, _ = sjson.SetRawBytes(message, "content", translatorcommon.JoinRawArray(contentItems))
message = translatorcommon.SetRawArrayItems(message, "content", contentItems)
} else if content.Type == gjson.String {
message, _ = sjson.SetBytes(message, "content", content.String())
}

View File

@@ -11,12 +11,24 @@ import (
)
func BenchmarkRequestTranslationLargeHistory(b *testing.B) {
benchmarkRequestTranslation(b, 64)
}
func BenchmarkRequestTranslationHistorySizes(b *testing.B) {
for _, turns := range []int{0, 1, 4, 16, 64} {
b.Run(fmt.Sprintf("turns_%d", turns), func(b *testing.B) {
benchmarkRequestTranslation(b, turns)
})
}
}
func benchmarkRequestTranslation(b *testing.B, turns int) {
requests := map[string][]byte{
"claude": benchmarkClaudeRequest(64),
"gemini": benchmarkGeminiRequest(64),
"openai": benchmarkOpenAIRequest(64),
"openai-response": benchmarkOpenAIResponsesRequest(64),
"interactions": benchmarkInteractionsRequest(64),
"claude": benchmarkClaudeRequest(turns),
"gemini": benchmarkGeminiRequest(turns),
"openai": benchmarkOpenAIRequest(turns),
"openai-response": benchmarkOpenAIResponsesRequest(turns),
"interactions": benchmarkInteractionsRequest(turns),
}
routes := []struct {
source string