perf(translator): add normalization and reuse logic for Codex request payloads

- Introduced field normalization functions (`setCodexRequiredBool`, `setCodexRequiredInclude`) to ensure correct Codex input structure.
- Added tests for normalized payload reuse and field validation in `ConvertOpenAIResponsesRequestToCodex`.
- Implemented `deleteCodexRequestFields` utility to remove unsupported fields dynamically.
- Added benchmarks for normalized payload processing to measure performance across different payload sizes.

Closes: #4362
This commit is contained in:
Luis Pater
2026-07-20 02:23:15 +08:00
parent fde40c5a0a
commit 0f52284e6c
2 changed files with 165 additions and 16 deletions
@@ -16,36 +16,73 @@ func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte,
if inputResult.Type == gjson.String {
input, _ := sjson.SetBytes([]byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`), "0.content.0.text", inputResult.String())
rawJSON, _ = sjson.SetRawBytes(rawJSON, "input", input)
inputResult = gjson.GetBytes(rawJSON, "input")
}
rawJSON, _ = sjson.SetBytes(rawJSON, "stream", true)
rawJSON, _ = sjson.SetBytes(rawJSON, "store", false)
rawJSON, _ = sjson.SetBytes(rawJSON, "parallel_tool_calls", true)
rawJSON, _ = sjson.SetBytes(rawJSON, "include", []string{"reasoning.encrypted_content"})
rawJSON = setCodexRequiredBool(rawJSON, "stream", true)
rawJSON = setCodexRequiredBool(rawJSON, "store", false)
rawJSON = setCodexRequiredBool(rawJSON, "parallel_tool_calls", true)
rawJSON = setCodexRequiredInclude(rawJSON)
// Codex Responses rejects token limit fields, so strip them out before forwarding.
rawJSON, _ = sjson.DeleteBytes(rawJSON, "max_output_tokens")
rawJSON, _ = sjson.DeleteBytes(rawJSON, "max_completion_tokens")
rawJSON, _ = sjson.DeleteBytes(rawJSON, "temperature")
rawJSON, _ = sjson.DeleteBytes(rawJSON, "top_p")
if v := gjson.GetBytes(rawJSON, "service_tier"); v.Exists() {
if v.String() != "priority" {
rawJSON, _ = sjson.DeleteBytes(rawJSON, "service_tier")
}
rawJSON = deleteCodexRequestFields(rawJSON, "max_output_tokens", "max_completion_tokens", "temperature", "top_p")
if serviceTier := gjson.GetBytes(rawJSON, "service_tier"); serviceTier.Exists() && serviceTier.String() != "priority" {
rawJSON = deleteCodexRequestFields(rawJSON, "service_tier")
}
rawJSON, _ = sjson.DeleteBytes(rawJSON, "truncation")
rawJSON = deleteCodexRequestFields(rawJSON, "truncation")
rawJSON = applyResponsesCompactionCompatibility(rawJSON)
// Delete the user field as it is not supported by the Codex upstream.
rawJSON, _ = sjson.DeleteBytes(rawJSON, "user")
rawJSON = deleteCodexRequestFields(rawJSON, "user")
// Convert role "system" to "developer" in input array to comply with Codex API requirements.
rawJSON = convertSystemRoleToDeveloper(rawJSON)
rawJSON = convertSystemRoleToDeveloperWithInput(rawJSON, inputResult)
rawJSON = normalizeCodexBuiltinTools(rawJSON)
return rawJSON
}
func setCodexRequiredBool(rawJSON []byte, path string, value bool) []byte {
current := gjson.GetBytes(rawJSON, path)
if value && current.Type == gjson.True || !value && current.Type == gjson.False {
return rawJSON
}
updated, errSet := sjson.SetBytes(rawJSON, path, value)
if errSet != nil {
return rawJSON
}
return updated
}
func setCodexRequiredInclude(rawJSON []byte) []byte {
current := gjson.GetBytes(rawJSON, "include")
values := current.Array()
if current.IsArray() && len(values) == 1 && values[0].Type == gjson.String && values[0].String() == "reasoning.encrypted_content" {
return rawJSON
}
updated, errSet := sjson.SetRawBytes(rawJSON, "include", []byte(`["reasoning.encrypted_content"]`))
if errSet != nil {
return rawJSON
}
return updated
}
func deleteCodexRequestFields(rawJSON []byte, paths ...string) []byte {
for _, path := range paths {
if !gjson.GetBytes(rawJSON, path).Exists() {
continue
}
updated, errDelete := sjson.DeleteBytes(rawJSON, path)
if errDelete == nil {
rawJSON = updated
}
}
return rawJSON
}
// applyResponsesCompactionCompatibility handles OpenAI Responses context_management.compaction
// for Codex upstream compatibility.
//
@@ -67,7 +104,10 @@ func applyResponsesCompactionCompatibility(rawJSON []byte) []byte {
// with role "system" to role "developer". This is necessary because Codex API does not
// accept "system" role in the input array.
func convertSystemRoleToDeveloper(rawJSON []byte) []byte {
inputResult := gjson.GetBytes(rawJSON, "input")
return convertSystemRoleToDeveloperWithInput(rawJSON, gjson.GetBytes(rawJSON, "input"))
}
func convertSystemRoleToDeveloperWithInput(rawJSON []byte, inputResult gjson.Result) []byte {
if !inputResult.IsArray() {
return rawJSON
}
@@ -77,6 +117,17 @@ func convertSystemRoleToDeveloper(rawJSON []byte) []byte {
return rawJSON
}
hasSystemRole := false
for _, item := range inputItems {
if item.IsObject() && item.Get("role").String() == "system" {
hasSystemRole = true
break
}
}
if !hasSystemRole {
return rawJSON
}
changed := false
rebuiltInput := make([]json.RawMessage, 0, len(inputItems))
for _, item := range inputItems {
@@ -11,6 +11,7 @@ import (
)
var benchmarkConvertSystemRoleOutput []byte
var benchmarkConvertNormalizedOutput []byte
// TestConvertSystemRoleToDeveloper_BasicConversion tests the basic system -> developer role conversion
func TestConvertSystemRoleToDeveloper_BasicConversion(t *testing.T) {
@@ -225,6 +226,69 @@ func TestConvertOpenAIResponsesRequestToCodex_OriginalIssue(t *testing.T) {
}
}
func TestConvertOpenAIResponsesRequestToCodexReusesNormalizedPayload(t *testing.T) {
inputJSON := []byte(`{"model":"gpt-5.6","stream":true,"store":false,"parallel_tool_calls":true,"include":["reasoning.encrypted_content"],"service_tier":"priority","input":[{"type":"message","role":"user","content":"hello"}]}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.6", inputJSON, true)
if &output[0] != &inputJSON[0] {
t.Fatal("normalized request payload was copied")
}
if string(output) != string(inputJSON) {
t.Fatalf("normalized request changed:\n got: %s\nwant: %s", output, inputJSON)
}
}
func TestConvertOpenAIResponsesRequestToCodexNormalizesRequiredFields(t *testing.T) {
inputJSON := []byte(`{
"model":"gpt-5.6",
"stream":"true",
"store":true,
"parallel_tool_calls":false,
"include":["file_search_call.results","reasoning.encrypted_content"],
"max_output_tokens":4096,
"max_completion_tokens":4096,
"temperature":0.2,
"top_p":0.9,
"service_tier":"standard",
"truncation":"auto",
"user":"request-owner",
"input":[{"type":"message","role":"system","content":"hello"}]
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.6", inputJSON, true)
if stream := gjson.GetBytes(output, "stream"); stream.Type != gjson.True {
t.Fatalf("stream = %s, want true", stream.Raw)
}
if store := gjson.GetBytes(output, "store"); store.Type != gjson.False {
t.Fatalf("store = %s, want false", store.Raw)
}
if parallel := gjson.GetBytes(output, "parallel_tool_calls"); parallel.Type != gjson.True {
t.Fatalf("parallel_tool_calls = %s, want true", parallel.Raw)
}
include := gjson.GetBytes(output, "include").Array()
if len(include) != 1 || include[0].Type != gjson.String || include[0].String() != "reasoning.encrypted_content" {
t.Fatalf("include = %s, want reasoning.encrypted_content only", gjson.GetBytes(output, "include").Raw)
}
if role := gjson.GetBytes(output, "input.0.role").String(); role != "developer" {
t.Fatalf("input.0.role = %q, want developer", role)
}
for _, path := range []string{
"max_output_tokens",
"max_completion_tokens",
"temperature",
"top_p",
"service_tier",
"truncation",
"user",
} {
if gjson.GetBytes(output, path).Exists() {
t.Fatalf("%s should be removed: %s", path, output)
}
}
}
// TestConvertSystemRoleToDeveloper_AssistantRole tests that assistant role is preserved
func TestConvertSystemRoleToDeveloper_AssistantRole(t *testing.T) {
inputJSON := []byte(`{
@@ -428,6 +492,40 @@ func BenchmarkConvertSystemRoleToDeveloperLargeInput(b *testing.B) {
}
}
func BenchmarkConvertOpenAIResponsesRequestToCodexNormalizedPayload(b *testing.B) {
cases := []struct {
name string
inputJSON []byte
}{
{name: "1KiB", inputJSON: makeNormalizedResponsesRequestForBenchmark(1 << 10)},
{name: "1MiB", inputJSON: makeNormalizedResponsesRequestForBenchmark(1 << 20)},
{name: "8MiB", inputJSON: makeNormalizedResponsesRequestForBenchmark(8 << 20)},
}
for _, testCase := range cases {
b.Run(testCase.name, func(b *testing.B) {
b.ReportAllocs()
b.SetBytes(int64(len(testCase.inputJSON)))
b.ResetTimer()
var output []byte
for b.Loop() {
output = ConvertOpenAIResponsesRequestToCodex("gpt-5.6", testCase.inputJSON, true)
}
benchmarkConvertNormalizedOutput = output
})
}
}
func makeNormalizedResponsesRequestForBenchmark(contentBytes int) []byte {
var builder strings.Builder
builder.Grow(contentBytes + 256)
builder.WriteString(`{"model":"gpt-5.6","stream":true,"store":false,"parallel_tool_calls":true,"include":["reasoning.encrypted_content"],"input":[{"type":"message","role":"user","content":"`)
builder.WriteString(strings.Repeat("x", contentBytes))
builder.WriteString(`"}]}`)
return []byte(builder.String())
}
func makeLargeResponsesInputForBenchmark(inputCount int, systemEvery int) []byte {
var builder strings.Builder
builder.Grow(inputCount * 96)