Merge pull request #4329 from mazzz1y/perf/executor

perf(translator): build input and messages arrays in a single pass
This commit is contained in:
Luis Pater
2026-07-19 04:06:09 +08:00
committed by GitHub
5 changed files with 132 additions and 71 deletions
@@ -163,39 +163,36 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
// Process messages and transform them to Claude Code format
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
messageIndex := 0
systemBlocks := make([][]byte, 0)
messageBlocks := make([][]byte, 0)
messages.ForEach(func(_, message gjson.Result) bool {
role := message.Get("role").String()
contentResult := message.Get("content")
switch role {
case "system":
systemStart := len(gjson.GetBytes(out, "system").Array())
systemStart := len(systemBlocks)
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
textPart := []byte(`{"type":"text","text":""}`)
textPart, _ = sjson.SetBytes(textPart, "text", contentResult.String())
textPart = common.AttachCacheControl(textPart, message)
out, _ = sjson.SetRawBytes(out, "system.-1", textPart)
systemBlocks = append(systemBlocks, textPart)
} else if contentResult.Exists() && contentResult.IsArray() {
contentResult.ForEach(func(_, part gjson.Result) bool {
if part.Get("type").String() == "text" {
textPart := []byte(`{"type":"text","text":""}`)
textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String())
textPart = common.AttachCacheControl(textPart, part)
out, _ = sjson.SetRawBytes(out, "system.-1", textPart)
systemBlocks = append(systemBlocks, textPart)
}
return true
})
// Message-level cache_control applies to the last system block from this message.
if message.Get("cache_control").Exists() {
systemArr := gjson.GetBytes(out, "system").Array()
if len(systemArr) > systemStart {
lastIdx := len(systemArr) - 1
if !systemArr[lastIdx].Get("cache_control").Exists() {
path := fmt.Sprintf("system.%d", lastIdx)
block := []byte(systemArr[lastIdx].Raw)
block = common.AttachCacheControl(block, message)
out, _ = sjson.SetRawBytes(out, path, block)
if len(systemBlocks) > systemStart {
lastIdx := len(systemBlocks) - 1
if !gjson.GetBytes(systemBlocks[lastIdx], "cache_control").Exists() {
systemBlocks[lastIdx] = common.AttachCacheControl(systemBlocks[lastIdx], message)
}
}
}
@@ -258,8 +255,7 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
}
msg = common.AttachMessageCacheControl(msg, message)
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
messageIndex++
messageBlocks = append(messageBlocks, msg)
case "tool":
// Handle tool result messages conversion
@@ -276,20 +272,22 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
msg, _ = sjson.SetBytes(msg, "content.0.content", toolResultContent)
}
msg = common.AttachMessageCacheControl(msg, message)
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
messageIndex++
messageBlocks = append(messageBlocks, msg)
}
return true
})
// Preserve a minimal conversational turn for system-only inputs.
// Claude payloads with top-level system instructions but no messages are risky for downstream validation.
if messageIndex == 0 {
system := gjson.GetBytes(out, "system")
if system.Exists() && system.IsArray() && len(system.Array()) > 0 {
fallbackMsg := []byte(`{"role":"user","content":[{"type":"text","text":""}]}`)
out, _ = sjson.SetRawBytes(out, "messages.-1", fallbackMsg)
}
if len(messageBlocks) == 0 && len(systemBlocks) > 0 {
messageBlocks = append(messageBlocks, []byte(`{"role":"user","content":[{"type":"text","text":""}]}`))
}
if len(systemBlocks) > 0 {
out, _ = sjson.SetRawBytes(out, "system", common.JoinRawArray(systemBlocks))
}
if len(messageBlocks) > 0 {
out, _ = sjson.SetRawBytes(out, "messages", common.JoinRawArray(messageBlocks))
}
}
+19
View File
@@ -22,6 +22,25 @@ func ClaudeInputTokensJSON(count int64) []byte {
return out
}
func JoinRawArray(items [][]byte) []byte {
if len(items) == 0 {
return []byte("[]")
}
size := len(items) + 1
for _, item := range items {
size += len(item)
}
out := make([]byte, 0, size)
out = append(out, '[')
for i, item := range items {
if i > 0 {
out = append(out, ',')
}
out = append(out, item...)
}
return append(out, ']')
}
func SSEEventData(event string, payload []byte) []byte {
out := make([]byte, 0, len(event)+len(payload)+14)
out = append(out, "event: "...)
+23
View File
@@ -0,0 +1,23 @@
package common
import "testing"
func TestJoinRawArray(t *testing.T) {
tests := []struct {
name string
items [][]byte
want string
}{
{name: "empty", want: "[]"},
{name: "single", items: [][]byte{[]byte(`{"id":1}`)}, want: `[{"id":1}]`},
{name: "multiple", items: [][]byte{[]byte(`{"id":1}`), []byte(`{"id":2}`)}, want: `[{"id":1},{"id":2}]`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := string(JoinRawArray(test.items)); got != test.want {
t.Fatalf("JoinRawArray() = %s, want %s", got, test.want)
}
})
}
}
@@ -3,6 +3,7 @@ package responses
import (
"strings"
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -21,7 +22,7 @@ func ConvertOpenAIResponsesRequestToInteractions(modelName string, inputRawJSON
out, _ = sjson.SetBytes(out, "previous_interaction_id", previousResponseID.String())
}
if input := root.Get("input"); input.Exists() {
out = appendResponsesInputToInteractions(out, input)
out = setResponsesInputOnInteractions(out, input)
}
out = appendResponsesToolsToInteractions(out, root.Get("tools"))
if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
@@ -55,7 +56,7 @@ func ConvertInteractionsRequestToOpenAIResponses(modelName string, inputRawJSON
out, _ = sjson.SetBytes(out, "previous_response_id", previousInteractionID.String())
}
if input := root.Get("input"); input.Exists() {
out = appendInteractionsInputToResponses(out, input)
out = setInteractionsInputOnResponses(out, input)
}
out = appendInteractionsToolsToResponses(out, root.Get("tools"))
if toolChoice := root.Get("generation_config.tool_choice"); toolChoice.Exists() {
@@ -156,25 +157,30 @@ func interactionsThinkingEffort(root gjson.Result) string {
return ""
}
func appendResponsesInputToInteractions(out []byte, input gjson.Result) []byte {
func setResponsesInputOnInteractions(out []byte, input gjson.Result) []byte {
functionNamesByCallID := make(map[string]string)
items := make([][]byte, 0)
if input.Type == gjson.String {
return appendInteractionsTextStep(out, "user_input", input.String())
}
if input.IsArray() {
items = append(items, interactionsTextStep("user_input", input.String()))
} else if input.IsArray() {
input.ForEach(func(_, item gjson.Result) bool {
out = appendResponsesInputItemToInteractions(out, item, functionNamesByCallID)
if converted := responsesInputItemToInteractions(item, functionNamesByCallID); converted != nil {
items = append(items, converted)
}
return true
})
return out
} else if input.IsObject() {
if converted := responsesInputItemToInteractions(input, functionNamesByCallID); converted != nil {
items = append(items, converted)
}
}
if input.IsObject() {
return appendResponsesInputItemToInteractions(out, input, functionNamesByCallID)
if len(items) > 0 {
out, _ = sjson.SetRawBytes(out, "input", translatorcommon.JoinRawArray(items))
}
return out
}
func appendResponsesInputItemToInteractions(out []byte, item gjson.Result, functionNamesByCallID map[string]string) []byte {
func responsesInputItemToInteractions(item gjson.Result, functionNamesByCallID map[string]string) []byte {
switch item.Get("type").String() {
case "message":
stepType := "user_input"
@@ -183,8 +189,7 @@ func appendResponsesInputItemToInteractions(out []byte, item gjson.Result, funct
}
step := []byte(`{"type":"","content":[]}`)
step, _ = sjson.SetBytes(step, "type", stepType)
step = appendResponsesContentToInteractions(step, item.Get("content"), stepType)
out, _ = sjson.SetRawBytes(out, "input.-1", step)
return appendResponsesContentToInteractions(step, item.Get("content"), stepType)
case "function_call":
callID := firstNonEmpty(item.Get("call_id").String(), item.Get("id").String())
if callID != "" {
@@ -192,15 +197,15 @@ func appendResponsesInputItemToInteractions(out []byte, item gjson.Result, funct
functionNamesByCallID[callID] = name
}
}
out, _ = sjson.SetRawBytes(out, "input.-1", responsesFunctionCallToInteractions(item))
return responsesFunctionCallToInteractions(item)
case "function_call_output":
out, _ = sjson.SetRawBytes(out, "input.-1", responsesFunctionOutputToInteractions(item, functionNamesByCallID))
return responsesFunctionOutputToInteractions(item, functionNamesByCallID)
case "input_text", "output_text", "text":
stepType := "user_input"
if item.Get("type").String() == "output_text" {
stepType = "model_output"
}
out = appendInteractionsTextStep(out, stepType, item.Get("text").String())
return interactionsTextStep(stepType, item.Get("text").String())
case "input_image", "output_image":
stepType := "user_input"
if item.Get("type").String() == "output_image" {
@@ -211,15 +216,14 @@ func appendResponsesInputItemToInteractions(out []byte, item gjson.Result, funct
if part, ok := responsesContentPartToInteractions(item); ok {
step, _ = sjson.SetRawBytes(step, "content.-1", part)
}
out, _ = sjson.SetRawBytes(out, "input.-1", step)
return step
default:
if content := item.Get("content"); content.Exists() {
step := []byte(`{"type":"user_input","content":[]}`)
step = appendResponsesContentToInteractions(step, content, "user_input")
out, _ = sjson.SetRawBytes(out, "input.-1", step)
return appendResponsesContentToInteractions(step, content, "user_input")
}
}
return out
return nil
}
func appendResponsesContentToInteractions(step []byte, content gjson.Result, stepType string) []byte {
@@ -317,12 +321,11 @@ func responsesFunctionOutputToInteractions(item gjson.Result, functionNamesByCal
return out
}
func appendInteractionsTextStep(out []byte, stepType, text string) []byte {
func interactionsTextStep(stepType, text string) []byte {
step := []byte(`{"type":"","content":[{"type":"text","text":""}]}`)
step, _ = sjson.SetBytes(step, "type", stepType)
step, _ = sjson.SetBytes(step, "content.0.text", text)
out, _ = sjson.SetRawBytes(out, "input.-1", step)
return out
return step
}
func appendResponsesToolsToInteractions(out []byte, tools gjson.Result) []byte {
@@ -380,44 +383,52 @@ func functionDeclarationFromTool(tool gjson.Result) ([]byte, bool) {
return out, true
}
func appendInteractionsInputToResponses(out []byte, input gjson.Result) []byte {
func setInteractionsInputOnResponses(out []byte, input gjson.Result) []byte {
items := make([][]byte, 0)
if input.Type == gjson.String {
item := []byte(`{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}`)
item, _ = sjson.SetBytes(item, "content.0.text", input.String())
out, _ = sjson.SetRawBytes(out, "input.-1", item)
return out
}
if input.IsArray() {
items = append(items, interactionsTextMessage(input.String()))
} else if input.IsArray() {
input.ForEach(func(_, item gjson.Result) bool {
out = appendInteractionsInputItemToResponses(out, item)
if converted := interactionsInputItemToResponses(item); converted != nil {
items = append(items, converted)
}
return true
})
return out
} else if input.IsObject() {
if converted := interactionsInputItemToResponses(input); converted != nil {
items = append(items, converted)
}
}
if input.IsObject() {
return appendInteractionsInputItemToResponses(out, input)
if len(items) > 0 {
out, _ = sjson.SetRawBytes(out, "input", translatorcommon.JoinRawArray(items))
}
return out
}
func appendInteractionsInputItemToResponses(out []byte, item gjson.Result) []byte {
func interactionsTextMessage(text string) []byte {
item := []byte(`{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}`)
item, _ = sjson.SetBytes(item, "content.0.text", text)
return item
}
func interactionsInputItemToResponses(item gjson.Result) []byte {
switch item.Get("type").String() {
case "user_input":
out, _ = sjson.SetRawBytes(out, "input.-1", interactionsMessageToResponses(item, "user"))
return interactionsMessageToResponses(item, "user")
case "model_output":
out, _ = sjson.SetRawBytes(out, "input.-1", interactionsMessageToResponses(item, "assistant"))
return interactionsMessageToResponses(item, "assistant")
case "thought":
out, _ = sjson.SetRawBytes(out, "input.-1", interactionsThoughtToResponses(item))
return interactionsThoughtToResponses(item)
case "function_call":
out, _ = sjson.SetRawBytes(out, "input.-1", interactionsFunctionCallToResponses(item))
return interactionsFunctionCallToResponses(item)
case "function_result":
out, _ = sjson.SetRawBytes(out, "input.-1", interactionsFunctionResultToResponses(item))
return interactionsFunctionResultToResponses(item)
default:
if item.Type == gjson.String {
return appendInteractionsInputToResponses(out, item)
return interactionsTextMessage(item.String())
}
}
return out
return nil
}
func interactionsMessageToResponses(item gjson.Result, role string) []byte {
@@ -3,6 +3,7 @@ package responses
import (
"strings"
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -33,6 +34,11 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
root := gjson.ParseBytes(rawJSON)
messages := make([][]byte, 0)
appendMessage := func(message []byte) {
messages = append(messages, message)
}
// Set model name
out, _ = sjson.SetBytes(out, "model", modelName)
@@ -52,7 +58,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
if instructions := root.Get("instructions"); instructions.Exists() {
systemMessage := []byte(`{"role":"system","content":""}`)
systemMessage, _ = sjson.SetBytes(systemMessage, "content", instructions.String())
out, _ = sjson.SetRawBytes(out, "messages.-1", systemMessage)
appendMessage(systemMessage)
}
// Convert input array to messages
@@ -91,7 +97,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
if reasoningContent := takePendingReasoningContent(); reasoningContent != "" {
assistantMessage, _ = sjson.SetBytes(assistantMessage, "reasoning_content", reasoningContent)
}
out, _ = sjson.SetRawBytes(out, "messages.-1", assistantMessage)
appendMessage(assistantMessage)
for _, id := range pendingToolCallIDs {
if strings.TrimSpace(id) == "" {
continue
@@ -103,7 +109,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
}
flushDeferredMessages := func() {
for _, message := range deferredMessages {
out, _ = sjson.SetRawBytes(out, "messages.-1", message)
appendMessage(message)
}
deferredMessages = deferredMessages[:0]
}
@@ -122,7 +128,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
deferredMessages = append(deferredMessages, message)
return
}
out, _ = sjson.SetRawBytes(out, "messages.-1", message)
appendMessage(message)
}
appendPendingReasoningMessage := func() {
reasoningContent := takePendingReasoningContent()
@@ -251,7 +257,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
toolMessage, _ = sjson.SetBytes(toolMessage, "content", output.String())
}
out, _ = sjson.SetRawBytes(out, "messages.-1", toolMessage)
appendMessage(toolMessage)
if callID != "" {
delete(awaitingToolOutputs, callID)
}
@@ -278,7 +284,7 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
callID := strings.TrimSpace(item.Get("call_id").String())
toolMessage, _ = sjson.SetBytes(toolMessage, "tool_call_id", callID)
toolMessage, _ = sjson.SetBytes(toolMessage, "content", responsesToolOutputText(item.Get("output")))
out, _ = sjson.SetRawBytes(out, "messages.-1", toolMessage)
appendMessage(toolMessage)
if callID != "" {
delete(awaitingToolOutputs, callID)
}
@@ -295,7 +301,11 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
msg := []byte(`{}`)
msg, _ = sjson.SetBytes(msg, "role", "user")
msg, _ = sjson.SetBytes(msg, "content", input.String())
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
appendMessage(msg)
}
if len(messages) > 0 {
out, _ = sjson.SetRawBytes(out, "messages", translatorcommon.JoinRawArray(messages))
}
// Convert tools from responses format to chat completions format.