feat(executor): sanitize and drop overlong encrypted reasoning IDs in Codex input processing

- Updated `SanitizeCodexInputItemIDs` to remove encrypted reasoning items with IDs exceeding the Codex length limit.
- Refactored logic to deterministically shorten other overlong input item IDs, ensuring compliance with the Codex format.
- Added new tests to verify dropping behavior for encrypted reasoning and shortening of lengthy IDs without encrypted content.
This commit is contained in:
Luis Pater
2026-07-20 02:02:17 +08:00
parent e47ffda75b
commit fde40c5a0a
4 changed files with 131 additions and 28 deletions
@@ -15,9 +15,11 @@ import (
"github.com/tidwall/gjson"
)
func TestCodexExecutorExecuteStreamShortensOverlongInputItemIDs(t *testing.T) {
func TestCodexExecutorExecuteStreamSanitizesOverlongInputItemIDs(t *testing.T) {
longReasoningItemID := "rs_" + strings.Repeat("a", 64)
longCallItemID := strings.Repeat("grok-call-item-", 6)
longOutputItemID := strings.Repeat("grok-output-item-", 6)
encryptedContent := validOpenAIResponsesReasoningEncryptedContentForTest()
var gotBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, errRead := io.ReadAll(r.Body)
@@ -35,6 +37,7 @@ func TestCodexExecutorExecuteStreamShortensOverlongInputItemIDs(t *testing.T) {
result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "gpt-5.4",
Payload: []byte(`{"model":"gpt-5.4","stream":true,"input":[` +
`{"type":"reasoning","id":"` + longReasoningItemID + `","encrypted_content":"` + encryptedContent + `","summary":[]},` +
`{"type":"function_call","id":"` + longCallItemID + `","call_id":"call-1","name":"lookup","arguments":"{}"},` +
`{"type":"function_call_output","id":"` + longOutputItemID + `","call_id":"call-1","output":"ok"},` +
`{"type":"message","id":"msg-1","role":"user","content":"continue"}]}`),
@@ -48,6 +51,13 @@ func TestCodexExecutorExecuteStreamShortensOverlongInputItemIDs(t *testing.T) {
for range result.Chunks {
}
if input := gjson.GetBytes(gotBody, "input").Array(); len(input) != 3 {
t.Fatalf("upstream input length = %d, want 3: %s", len(input), gotBody)
}
if gotType := gjson.GetBytes(gotBody, "input.0.type").String(); gotType != "function_call" {
t.Fatalf("input.0.type = %q, want function_call: %s", gotType, gotBody)
}
for index, testCase := range []struct {
path string
originalID string
@@ -41,14 +41,22 @@ func TestBuildCodexWebsocketRequestBodyPreservesPreviousResponseID(t *testing.T)
}
}
func TestBuildCodexWebsocketRequestBodyShortensOverlongInputItemIDs(t *testing.T) {
func TestBuildCodexWebsocketRequestBodySanitizesOverlongInputItemIDs(t *testing.T) {
longReasoningItemID := "rs_" + strings.Repeat("a", 64)
longCallItemID := strings.Repeat("grok-call-item-", 6)
longOutputItemID := strings.Repeat("grok-output-item-", 6)
body := []byte(`{"model":"gpt-5-codex","input":[{"type":"function_call","id":"` + longCallItemID + `","call_id":"call-1","name":"lookup"},{"type":"function_call_output","id":"` + longOutputItemID + `","call_id":"call-1","output":"ok"},{"type":"message","id":"msg-1"}]}`)
body := []byte(`{"model":"gpt-5-codex","input":[{"type":"reasoning","id":"` + longReasoningItemID + `","encrypted_content":"gAAAA-encrypted","summary":[]},{"type":"function_call","id":"` + longCallItemID + `","call_id":"call-1","name":"lookup"},{"type":"function_call_output","id":"` + longOutputItemID + `","call_id":"call-1","output":"ok"},{"type":"message","id":"msg-1"}]}`)
first := buildCodexWebsocketRequestBody(body)
second := buildCodexWebsocketRequestBody(body)
if input := gjson.GetBytes(first, "input").Array(); len(input) != 3 {
t.Fatalf("input length = %d, want 3: %s", len(input), first)
}
if gotType := gjson.GetBytes(first, "input.0.type").String(); gotType != "function_call" {
t.Fatalf("input.0.type = %q, want function_call: %s", gotType, first)
}
shortCallItemID := gjson.GetBytes(first, "input.0.id").String()
shortOutputItemID := gjson.GetBytes(first, "input.1.id").String()
if len([]rune(shortCallItemID)) > 64 || shortCallItemID == longCallItemID {
@@ -4,6 +4,7 @@ import (
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
@@ -11,7 +12,8 @@ import (
const codexInputItemIDLimit = 64
// SanitizeCodexInputItemIDs deterministically shortens overlong Responses input item IDs.
// SanitizeCodexInputItemIDs removes encrypted reasoning items whose IDs exceed
// the Codex limit and deterministically shortens other overlong input item IDs.
func SanitizeCodexInputItemIDs(body []byte) []byte {
input := gjson.GetBytes(body, "input")
if !input.IsArray() {
@@ -21,6 +23,9 @@ func SanitizeCodexInputItemIDs(body []byte) []byte {
items := input.Array()
occupied := make(map[string]struct{}, len(items))
for _, item := range items {
if shouldDropCodexEncryptedReasoningItem(item) {
continue
}
itemID := item.Get("id")
if itemID.Type != gjson.String {
continue
@@ -32,38 +37,64 @@ func SanitizeCodexInputItemIDs(body []byte) []byte {
}
mapped := make(map[string]string, len(items))
updated := body
for index, item := range items {
rebuilt := make([]string, 0, len(items))
changed := false
for _, item := range items {
if shouldDropCodexEncryptedReasoningItem(item) {
changed = true
continue
}
raw := item.Raw
itemID := item.Get("id")
if itemID.Type != gjson.String {
continue
}
id := itemID.String()
if len([]rune(id)) <= codexInputItemIDLimit {
continue
}
shortened, ok := mapped[id]
if !ok {
shortened = shortenCodexInputItemID(id)
for attempt := 1; ; attempt++ {
if _, exists := occupied[shortened]; !exists {
break
if itemID.Type == gjson.String {
id := itemID.String()
if len([]rune(id)) > codexInputItemIDLimit {
shortened, ok := mapped[id]
if !ok {
shortened = shortenCodexInputItemID(id)
for attempt := 1; ; attempt++ {
if _, exists := occupied[shortened]; !exists {
break
}
shortened = shortenCodexInputItemIDWithAttempt(id, attempt)
}
mapped[id] = shortened
occupied[shortened] = struct{}{}
}
shortened = shortenCodexInputItemIDWithAttempt(id, attempt)
}
mapped[id] = shortened
occupied[shortened] = struct{}{}
}
next, errSet := sjson.SetBytes(updated, "input."+strconv.Itoa(index)+".id", shortened)
if errSet == nil {
updated = next
next, errSet := sjson.SetBytes([]byte(raw), "id", shortened)
if errSet == nil {
raw = string(next)
changed = true
}
}
}
rebuilt = append(rebuilt, raw)
}
if !changed {
return body
}
updated, errSet := sjson.SetRawBytes(body, "input", []byte("["+strings.Join(rebuilt, ",")+"]"))
if errSet != nil {
return body
}
return updated
}
func shouldDropCodexEncryptedReasoningItem(item gjson.Result) bool {
if item.Get("type").String() != "reasoning" {
return false
}
itemID := item.Get("id")
if itemID.Type != gjson.String || len([]rune(itemID.String())) <= codexInputItemIDLimit {
return false
}
encryptedContent := item.Get("encrypted_content")
return encryptedContent.Type == gjson.String && encryptedContent.String() != ""
}
func shortenCodexInputItemID(id string) string {
return shortenCodexInputItemIDWithAttempt(id, 0)
}
@@ -26,6 +26,60 @@ func TestSanitizeCodexInputItemIDsBoundaries(t *testing.T) {
}
}
func TestSanitizeCodexInputItemIDsDropsOverlongEncryptedReasoningItem(t *testing.T) {
longReasoningID := "rs_" + strings.Repeat("a", 64)
shortReasoningID := "rs_" + strings.Repeat("b", 48)
longCallID := strings.Repeat("call-item-", 8)
body := []byte(`{"input":[` +
`{"type":"message","id":"msg-1","role":"user","content":"before"},` +
`{"type":"reasoning","id":"` + longReasoningID + `","encrypted_content":"gAAAA-encrypted","summary":[{"type":"summary_text","text":"drop me"}]},` +
`{"type":"reasoning","id":"` + shortReasoningID + `","encrypted_content":"gAAAA-encrypted","summary":[]},` +
`{"type":"function_call","id":"` + longCallID + `","call_id":"call-1","name":"lookup","arguments":"{}"}` +
`]}`)
got := SanitizeCodexInputItemIDs(body)
input := gjson.GetBytes(got, "input").Array()
if len(input) != 3 {
t.Fatalf("input length = %d, want 3: %s", len(input), got)
}
if gotID := input[0].Get("id").String(); gotID != "msg-1" {
t.Fatalf("input.0.id = %q, want msg-1", gotID)
}
if gotID := input[1].Get("id").String(); gotID != shortReasoningID {
t.Fatalf("short encrypted reasoning id changed: %q", gotID)
}
if gotID := input[2].Get("id").String(); gotID == longCallID || len([]rune(gotID)) != 64 {
t.Fatalf("ordinary overlong id was not shortened: %q", gotID)
}
}
func TestSanitizeCodexInputItemIDsShortensOverlongReasoningWithoutEncryptedContent(t *testing.T) {
longReasoningID := "rs_" + strings.Repeat("a", 64)
for _, testCase := range []struct {
name string
encryptedContent string
}{
{name: "missing"},
{name: "empty", encryptedContent: `,"encrypted_content":""`},
{name: "null", encryptedContent: `,"encrypted_content":null`},
} {
t.Run(testCase.name, func(t *testing.T) {
body := []byte(`{"input":[{"type":"reasoning","id":"` + longReasoningID + `"` + testCase.encryptedContent + `,"summary":[]}]}`)
got := SanitizeCodexInputItemIDs(body)
input := gjson.GetBytes(got, "input").Array()
if len(input) != 1 {
t.Fatalf("input length = %d, want 1: %s", len(input), got)
}
gotID := input[0].Get("id").String()
if gotID == longReasoningID || len([]rune(gotID)) != 64 {
t.Fatalf("overlong reasoning id was not shortened: %q", gotID)
}
})
}
}
func TestSanitizeCodexInputItemIDsAvoidsExistingIDCollision(t *testing.T) {
longID := strings.Repeat("grok-item-", 10)
collidingValidID := shortenCodexInputItemID(longID)