mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user