mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:48:45 +08:00
fix(openai): recover stale encrypted compaction
Reason: - Responses retries can carry account-bound encrypted compaction items that OpenAI rejects with invalid_encrypted_content. Changes: - Drop encrypted compaction and compaction_summary items only during the existing recovery retry. - Preserve unencrypted compaction items and cover HTTP and WebSocket recovery paths.
This commit is contained in:
@@ -132,7 +132,14 @@ func sanitizeEncryptedReasoningInputItem(item any) (next any, changed bool, keep
|
||||
}
|
||||
|
||||
itemType, _ := inputItem["type"].(string)
|
||||
if strings.TrimSpace(itemType) != "reasoning" {
|
||||
switch strings.TrimSpace(itemType) {
|
||||
case "compaction", "compaction_summary":
|
||||
if _, encrypted := inputItem["encrypted_content"]; encrypted {
|
||||
return nil, true, false
|
||||
}
|
||||
return item, false, true
|
||||
case "reasoning":
|
||||
default:
|
||||
return item, false, true
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,47 @@ func TestTrimOpenAIEncryptedReasoningItems_NoReasoningItems(t *testing.T) {
|
||||
assert.False(t, changed)
|
||||
}
|
||||
|
||||
func TestTrimOpenAIEncryptedReasoningItems_Compaction(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
itemType string
|
||||
encrypted bool
|
||||
changed bool
|
||||
}{
|
||||
{name: "compaction", itemType: "compaction", encrypted: true, changed: true},
|
||||
{name: "compaction summary", itemType: "compaction_summary", encrypted: true, changed: true},
|
||||
{name: "unencrypted compaction", itemType: "compaction", encrypted: false, changed: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
item := map[string]any{"type": tt.itemType, "id": "cmp_stale"}
|
||||
if tt.encrypted {
|
||||
item["encrypted_content"] = "gAAA"
|
||||
}
|
||||
reqBody := map[string]any{"input": []any{
|
||||
item,
|
||||
map[string]any{"type": "message", "content": "hi"},
|
||||
}}
|
||||
|
||||
changed := trimOpenAIEncryptedReasoningItems(reqBody)
|
||||
assert.Equal(t, tt.changed, changed)
|
||||
input, ok := reqBody["input"].([]any)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, input)
|
||||
first, ok := input[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
if tt.changed {
|
||||
require.Len(t, input, 1)
|
||||
assert.Equal(t, "message", first["type"])
|
||||
return
|
||||
}
|
||||
require.Len(t, input, 2)
|
||||
assert.Equal(t, tt.itemType, first["type"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeOpenAICrossModeFailoverReasoning_DropsWholeEncryptedItem(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.1","input":[` +
|
||||
`{"type":"message","role":"user","content":"hi"},` +
|
||||
|
||||
@@ -593,6 +593,52 @@ func TestOpenAIGatewayService_Forward_HTTPRetryRecoveryDoesNotDecodeBeforeError(
|
||||
require.Equal(t, "summary_text", gjson.GetBytes(upstream.bodies[1], "input.0.summary.0.type").String())
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayService_Forward_HTTPRetryRecoveryDropsCompaction(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
upstream := &httpUpstreamRecorder{
|
||||
responses: []*http.Response{
|
||||
{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"error":{"code":"invalid_encrypted_content","type":"invalid_request_error","message":"bad encrypted content"}}`)),
|
||||
},
|
||||
{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"usage":{"input_tokens":1,"output_tokens":2}}`)),
|
||||
},
|
||||
},
|
||||
}
|
||||
cfg := &config.Config{}
|
||||
cfg.Security.URLAllowlist.Enabled = false
|
||||
svc := &OpenAIGatewayService{cfg: cfg, httpUpstream: upstream}
|
||||
account := &Account{
|
||||
ID: 10,
|
||||
Name: "openai-apikey",
|
||||
Platform: PlatformOpenAI,
|
||||
Type: AccountTypeAPIKey,
|
||||
Concurrency: 1,
|
||||
Credentials: map[string]any{
|
||||
"api_key": "sk-test",
|
||||
"base_url": "https://example.com",
|
||||
},
|
||||
Extra: map[string]any{"use_responses_api": true},
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", nil)
|
||||
SetOpenAIClientTransport(c, OpenAIClientTransportHTTP)
|
||||
|
||||
body := []byte(`{"model":"gpt-5.6-sol","stream":false,"input":[{"id":"cmp_stale","type":"compaction","encrypted_content":"gAAA"},{"type":"message","content":[{"type":"input_text","text":"hi"}]}]}`)
|
||||
result, err := svc.Forward(context.Background(), c, account, body)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Len(t, upstream.bodies, 2)
|
||||
require.Equal(t, "compaction", gjson.GetBytes(upstream.bodies[0], "input.0.type").String())
|
||||
require.Equal(t, "message", gjson.GetBytes(upstream.bodies[1], "input.0.type").String())
|
||||
require.False(t, gjson.GetBytes(upstream.bodies[1], "input.1").Exists())
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayService_Forward_CodexSparkRejectsEscapedInputImage(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
upstream := &httpUpstreamRecorder{
|
||||
|
||||
@@ -1537,7 +1537,7 @@ func TestOpenAIGatewayService_Forward_WSv2InvalidEncryptedContentRecoversOnce(t
|
||||
},
|
||||
}
|
||||
|
||||
body := []byte(`{"model":"gpt-5.3-codex","stream":false,"previous_response_id":"resp_prev_encrypted","input":[{"type":"reasoning","encrypted_content":"gAAA"},{"type":"input_text","text":"hello"}]}`)
|
||||
body := []byte(`{"model":"gpt-5.3-codex","stream":false,"previous_response_id":"resp_prev_encrypted","input":[{"type":"reasoning","encrypted_content":"gAAA"},{"type":"compaction","encrypted_content":"cAAA"},{"type":"input_text","text":"hello"}]}`)
|
||||
result, err := svc.Forward(context.Background(), c, account, body)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
@@ -1553,6 +1553,7 @@ func TestOpenAIGatewayService_Forward_WSv2InvalidEncryptedContentRecoversOnce(t
|
||||
require.Len(t, requests, 2)
|
||||
require.True(t, gjson.GetBytes(requests[0], "previous_response_id").Exists(), "首轮请求应保留 previous_response_id")
|
||||
require.True(t, gjson.GetBytes(requests[0], `input.0.encrypted_content`).Exists(), "首轮请求应保留 encrypted reasoning")
|
||||
require.True(t, gjson.GetBytes(requests[0], `input.1.encrypted_content`).Exists(), "首轮请求应保留 encrypted compaction")
|
||||
require.False(t, gjson.GetBytes(requests[1], "previous_response_id").Exists(), "恢复重试应移除 previous_response_id")
|
||||
require.False(t, gjson.GetBytes(requests[1], `input.0.encrypted_content`).Exists(), "恢复重试应移除 encrypted reasoning item")
|
||||
require.Equal(t, "input_text", gjson.GetBytes(requests[1], `input.0.type`).String())
|
||||
|
||||
Reference in New Issue
Block a user