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