diff --git a/internal/runtime/executor/codex_executor_input_ids_test.go b/internal/runtime/executor/codex_executor_input_ids_test.go index 15b0fe68..f024a8bf 100644 --- a/internal/runtime/executor/codex_executor_input_ids_test.go +++ b/internal/runtime/executor/codex_executor_input_ids_test.go @@ -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 diff --git a/internal/runtime/executor/codex_websockets_executor_test.go b/internal/runtime/executor/codex_websockets_executor_test.go index 79cba3f1..555f8f62 100644 --- a/internal/runtime/executor/codex_websockets_executor_test.go +++ b/internal/runtime/executor/codex_websockets_executor_test.go @@ -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 { diff --git a/internal/runtime/executor/helps/codex_input_ids.go b/internal/runtime/executor/helps/codex_input_ids.go index 4f4122c8..09e16cc9 100644 --- a/internal/runtime/executor/helps/codex_input_ids.go +++ b/internal/runtime/executor/helps/codex_input_ids.go @@ -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 { - itemID := item.Get("id") - if itemID.Type != gjson.String { - continue - } - id := itemID.String() - if len([]rune(id)) <= codexInputItemIDLimit { + rebuilt := make([]string, 0, len(items)) + changed := false + for _, item := range items { + if shouldDropCodexEncryptedReasoningItem(item) { + changed = true continue } - shortened, ok := mapped[id] - if !ok { - shortened = shortenCodexInputItemID(id) - for attempt := 1; ; attempt++ { - if _, exists := occupied[shortened]; !exists { - break + raw := item.Raw + itemID := item.Get("id") + 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{}{} + } + + next, errSet := sjson.SetBytes([]byte(raw), "id", shortened) + if errSet == nil { + raw = string(next) + changed = true } - shortened = shortenCodexInputItemIDWithAttempt(id, attempt) } - mapped[id] = shortened - occupied[shortened] = struct{}{} } + rebuilt = append(rebuilt, raw) + } + if !changed { + return body + } - next, errSet := sjson.SetBytes(updated, "input."+strconv.Itoa(index)+".id", shortened) - if errSet == nil { - updated = next - } + 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) } diff --git a/internal/runtime/executor/helps/codex_input_ids_test.go b/internal/runtime/executor/helps/codex_input_ids_test.go index 4d833449..64debae5 100644 --- a/internal/runtime/executor/helps/codex_input_ids_test.go +++ b/internal/runtime/executor/helps/codex_input_ids_test.go @@ -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)