From 7c61e982e490f028d295d69e22e372b29cd2db8c Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sun, 19 Jul 2026 15:01:37 +0800 Subject: [PATCH] perf(translator): optimize array allocation logic and replace `JoinRawArray` with `SetRawArrayItems` methods --- .../claude/antigravity_claude_request.go | 6 ++-- .../gemini/antigravity_gemini_request.go | 12 +++---- .../interactions_antigravity_request.go | 10 +++--- .../antigravity_openai_request.go | 2 +- .../claude/gemini/claude_gemini_request.go | 4 +-- .../interactions_claude_request.go | 6 ++-- .../chat-completions/claude_openai_request.go | 4 +-- .../claude_openai-responses_request.go | 10 ++++-- .../codex/claude/codex_claude_request.go | 4 +-- .../codex/gemini/codex_gemini_request.go | 6 ++-- .../interactions_codex_request.go | 4 +-- .../chat-completions/codex_openai_request.go | 4 +-- .../codex_openai-responses_request.go | 2 +- internal/translator/common/bytes.go | 32 ++++++++++++++++++ internal/translator/common/bytes_test.go | 33 +++++++++++++++++++ .../gemini/claude/gemini_claude_request.go | 6 ++-- .../gemini/gemini/gemini_gemini_request.go | 8 ++--- .../interactions_gemini_common.go | 10 +++--- .../chat-completions/gemini_openai_request.go | 2 +- .../gemini_openai-responses_request.go | 6 ++-- .../claude/interactions_claude_request.go | 6 ++-- .../openai/claude/openai_claude_request.go | 10 ++++-- .../openai/gemini/openai_gemini_request.go | 10 ++++-- .../interactions_openai_request.go | 12 ++++--- .../openai_interactions_request.go | 6 ++-- .../interactions_openai_responses_request.go | 16 ++++----- .../openai_openai-responses_request.go | 4 +-- internal/translator/request_benchmark_test.go | 22 ++++++++++--- 28 files changed, 175 insertions(+), 82 deletions(-) diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index 3e8974939..a33aa333c 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -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) diff --git a/internal/translator/antigravity/gemini/antigravity_gemini_request.go b/internal/translator/antigravity/gemini/antigravity_gemini_request.go index ab2c057d5..4fc4a0d90 100644 --- a/internal/translator/antigravity/gemini/antigravity_gemini_request.go +++ b/internal/translator/antigravity/gemini/antigravity_gemini_request.go @@ -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) { diff --git a/internal/translator/antigravity/interactions/interactions_antigravity_request.go b/internal/translator/antigravity/interactions/interactions_antigravity_request.go index 48b41a16d..4b420ef1c 100644 --- a/internal/translator/antigravity/interactions/interactions_antigravity_request.go +++ b/internal/translator/antigravity/interactions/interactions_antigravity_request.go @@ -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 { diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go index 718fd93dd..d8cdf9933 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go @@ -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 diff --git a/internal/translator/claude/gemini/claude_gemini_request.go b/internal/translator/claude/gemini/claude_gemini_request.go index bcbdaaf47..9ed4d6206 100644 --- a/internal/translator/claude/gemini/claude_gemini_request.go +++ b/internal/translator/claude/gemini/claude_gemini_request.go @@ -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_ // 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() { diff --git a/internal/translator/claude/interactions/interactions_claude_request.go b/internal/translator/claude/interactions/interactions_claude_request.go index e5cd22e44..0f0e8e998 100644 --- a/internal/translator/claude/interactions/interactions_claude_request.go +++ b/internal/translator/claude/interactions/interactions_claude_request.go @@ -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 { diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request.go b/internal/translator/claude/openai/chat-completions/claude_openai_request.go index 949b62b4d..7d16eaf6a 100644 --- a/internal/translator/claude/openai/chat-completions/claude_openai_request.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go @@ -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") diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_request.go b/internal/translator/claude/openai/responses/claude_openai-responses_request.go index a741619a4..57d536450 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_request.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_request.go @@ -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 { diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index 66da8036d..888bfa450 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -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 } diff --git a/internal/translator/codex/gemini/codex_gemini_request.go b/internal/translator/codex/gemini/codex_gemini_request.go index ae5d049a0..a53a067ec 100644 --- a/internal/translator/codex/gemini/codex_gemini_request.go +++ b/internal/translator/codex/gemini/codex_gemini_request.go @@ -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++ { diff --git a/internal/translator/codex/interactions/interactions_codex_request.go b/internal/translator/codex/interactions/interactions_codex_request.go index 0b11141af..e37ad33f7 100644 --- a/internal/translator/codex/interactions/interactions_codex_request.go +++ b/internal/translator/codex/interactions/interactions_codex_request.go @@ -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 diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_request.go b/internal/translator/codex/openai/chat-completions/codex_openai_request.go index b5265b651..d4436d972 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_request.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_request.go @@ -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") diff --git a/internal/translator/codex/openai/responses/codex_openai-responses_request.go b/internal/translator/codex/openai/responses/codex_openai-responses_request.go index eccf4fc0d..bcf6ab54c 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request.go @@ -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() diff --git a/internal/translator/common/bytes.go b/internal/translator/common/bytes.go index 3b5ad8140..76c4c0ae6 100644 --- a/internal/translator/common/bytes.go +++ b/internal/translator/common/bytes.go @@ -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: "...) diff --git a/internal/translator/common/bytes_test.go b/internal/translator/common/bytes_test.go index 2ce967054..eb8ba7f4d 100644 --- a/internal/translator/common/bytes_test.go +++ b/internal/translator/common/bytes_test.go @@ -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) + } + }) + } +} diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index 4be8a3721..f6641a5ea 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -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() { diff --git a/internal/translator/gemini/gemini/gemini_gemini_request.go b/internal/translator/gemini/gemini/gemini_gemini_request.go index 9763d3109..50d03c144 100644 --- a/internal/translator/gemini/gemini/gemini_gemini_request.go +++ b/internal/translator/gemini/gemini/gemini_gemini_request.go @@ -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 { diff --git a/internal/translator/gemini/interactions/interactions_gemini_common.go b/internal/translator/gemini/interactions/interactions_gemini_common.go index 43dc7be30..efc083970 100644 --- a/internal/translator/gemini/interactions/interactions_gemini_common.go +++ b/internal/translator/gemini/interactions/interactions_gemini_common.go @@ -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 } diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index 37036906f..a468b6d4f 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -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 diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index 3c7ce1cad..21e2d2e6b 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -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":{}}`) diff --git a/internal/translator/interactions/claude/interactions_claude_request.go b/internal/translator/interactions/claude/interactions_claude_request.go index 20cc31e3e..8f6c2746b 100644 --- a/internal/translator/interactions/claude/interactions_claude_request.go +++ b/internal/translator/interactions/claude/interactions_claude_request.go @@ -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 == "" { diff --git a/internal/translator/openai/claude/openai_claude_request.go b/internal/translator/openai/claude/openai_claude_request.go index 71378091d..68848854f 100644 --- a/internal/translator/openai/claude/openai_claude_request.go +++ b/internal/translator/openai/claude/openai_claude_request.go @@ -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()) diff --git a/internal/translator/openai/gemini/openai_gemini_request.go b/internal/translator/openai/gemini/openai_gemini_request.go index 9e607523c..e7cf18a3c 100644 --- a/internal/translator/openai/gemini/openai_gemini_request.go +++ b/internal/translator/openai/gemini/openai_gemini_request.go @@ -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 { diff --git a/internal/translator/openai/interactions/chat-completions/interactions_openai_request.go b/internal/translator/openai/interactions/chat-completions/interactions_openai_request.go index 78c19d908..7688de34b 100644 --- a/internal/translator/openai/interactions/chat-completions/interactions_openai_request.go +++ b/internal/translator/openai/interactions/chat-completions/interactions_openai_request.go @@ -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) diff --git a/internal/translator/openai/interactions/chat-completions/openai_interactions_request.go b/internal/translator/openai/interactions/chat-completions/openai_interactions_request.go index 2f2d22799..7d25a9cbb 100644 --- a/internal/translator/openai/interactions/chat-completions/openai_interactions_request.go +++ b/internal/translator/openai/interactions/chat-completions/openai_interactions_request.go @@ -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) diff --git a/internal/translator/openai/interactions/responses/interactions_openai_responses_request.go b/internal/translator/openai/interactions/responses/interactions_openai_responses_request.go index 22489fa83..2fe0c0103 100644 --- a/internal/translator/openai/interactions/responses/interactions_openai_responses_request.go +++ b/internal/translator/openai/interactions/responses/interactions_openai_responses_request.go @@ -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) diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_request.go b/internal/translator/openai/openai/responses/openai_openai-responses_request.go index 53fa08679..1e9f9b1ed 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_request.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_request.go @@ -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()) } diff --git a/internal/translator/request_benchmark_test.go b/internal/translator/request_benchmark_test.go index d03934f62..333a4a01a 100644 --- a/internal/translator/request_benchmark_test.go +++ b/internal/translator/request_benchmark_test.go @@ -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