From 196c15b5cd26436c209e50466ec1b4fe1a98f8c9 Mon Sep 17 00:00:00 2001 From: r266-tech Date: Sun, 13 Sep 2026 07:00:50 +0800 Subject: [PATCH] fix(openai): strip internal metadata from OAuth input items --- .../service/openai_gateway_request_body.go | 29 +++++++ .../openai_oauth_input_metadata_test.go | 82 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 backend/internal/service/openai_oauth_input_metadata_test.go diff --git a/backend/internal/service/openai_gateway_request_body.go b/backend/internal/service/openai_gateway_request_body.go index e26197ed1e..27306e34a4 100644 --- a/backend/internal/service/openai_gateway_request_body.go +++ b/backend/internal/service/openai_gateway_request_body.go @@ -1062,6 +1062,19 @@ func normalizeOpenAIOAuthResponsesCompatibilityFields(reqBody map[string]any) bo delete(reqBody, "commands") changed = true } + // Codex can attach internal message metadata when a custom provider is + // named OpenAI. ChatGPT rejects this field on input items (#7066). + input, _ := reqBody["input"].([]any) + for _, value := range input { + item, ok := value.(map[string]any) + if !ok { + continue + } + if _, exists := item["internal_chat_message_metadata_passthrough"]; exists { + delete(item, "internal_chat_message_metadata_passthrough") + changed = true + } + } return changed } @@ -1096,6 +1109,22 @@ func normalizeOpenAIOAuthResponsesCompatibilityBody(body []byte) ([]byte, bool, normalized = next changed = true } + // Only remove the input-item field, never same-named user content. + input := gjson.GetBytes(normalized, "input") + if !input.IsArray() { + return normalized, changed, nil + } + for i, item := range input.Array() { + if !item.IsObject() || !item.Get("internal_chat_message_metadata_passthrough").Exists() { + continue + } + next, err := sjson.DeleteBytes(normalized, fmt.Sprintf("input.%d.internal_chat_message_metadata_passthrough", i)) + if err != nil { + return body, false, fmt.Errorf("normalize oauth input metadata: %w", err) + } + normalized = next + changed = true + } return normalized, changed, nil } diff --git a/backend/internal/service/openai_oauth_input_metadata_test.go b/backend/internal/service/openai_oauth_input_metadata_test.go new file mode 100644 index 0000000000..2fbdfa537c --- /dev/null +++ b/backend/internal/service/openai_oauth_input_metadata_test.go @@ -0,0 +1,82 @@ +package service + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestOAuthInputInternalMetadata(t *testing.T) { + const field = "internal_chat_message_metadata_passthrough" + // The identically named field in user content must not be recursively stripped. + const body = `{"model":"gpt-5.5","input":[{"role":"user","content":[{"type":"input_text","text":"hello","internal_chat_message_metadata_passthrough":{"keep":true}}],"internal_chat_message_metadata_passthrough":{"content_item_kinds":["text"]}},{"type":"function_call","call_id":"fc_123","name":"echo","arguments":"{\"internal_chat_message_metadata_passthrough\":true}","internal_chat_message_metadata_passthrough":null},{"type":"function_call_output","call_id":"fc_123","output":"result"}],"internal_chat_message_metadata_passthrough":{"keep":true}}` + tests := []struct { + name string + normalize func([]byte) ([]byte, bool, error) + strip bool + }{ + {"transformed OAuth", func(b []byte) ([]byte, bool, error) { + var req map[string]any + if err := json.Unmarshal(b, &req); err != nil { + return nil, false, err + } + result := applyCodexOAuthTransform(req, false, false) + out, err := json.Marshal(req) + return out, result.Modified, err + }, true}, + {"OAuth passthrough", func(b []byte) ([]byte, bool, error) { return normalizeOpenAIPassthroughOAuthBody(b, false) }, true}, + {"OAuth compact", func(b []byte) ([]byte, bool, error) { return normalizeOpenAIPassthroughOAuthBody(b, true) }, true}, + {"OAuth websocket", func(b []byte) ([]byte, bool, error) { + return normalizeOpenAIResponsesWebSocketCompatibilityBody(b, &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}, false) + }, true}, + {"API key websocket", func(b []byte) ([]byte, bool, error) { + return normalizeOpenAIResponsesWebSocketCompatibilityBody(b, &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, false) + }, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out, _, err := tt.normalize([]byte(body)) + require.NoError(t, err) + require.Equal(t, !tt.strip, gjson.GetBytes(out, "input.0."+field).Exists()) + require.Equal(t, !tt.strip, gjson.GetBytes(out, "input.1."+field).Exists()) + require.Equal(t, "hello", gjson.GetBytes(out, "input.0.content.0.text").String()) + require.True(t, gjson.GetBytes(out, "input.0.content.0."+field+".keep").Bool()) + require.True(t, gjson.GetBytes(out, field+".keep").Bool()) + require.Equal(t, `{"internal_chat_message_metadata_passthrough":true}`, gjson.GetBytes(out, "input.1.arguments").String()) + require.Equal(t, "fc_123", gjson.GetBytes(out, "input.1.call_id").String()) + require.Equal(t, "result", gjson.GetBytes(out, "input.2.output").String()) + }) + } +} + +func TestOAuthInputInternalMetadataCompatibility(t *testing.T) { + for _, body := range []string{ + `{"input":"hello"}`, + `{"input":{"internal_chat_message_metadata_passthrough":true}}`, + `{"input":[null,"hello",{"role":"user","content":"hello"}]}`, + `{"input":[{"content":{"internal_chat_message_metadata_passthrough":true}}]}`, + } { + t.Run(body, func(t *testing.T) { + out, changed, err := normalizeOpenAIOAuthResponsesCompatibilityBody([]byte(body)) + require.NoError(t, err) + require.False(t, changed) + require.Equal(t, body, string(out)) + var req map[string]any + require.NoError(t, json.Unmarshal([]byte(body), &req)) + require.False(t, normalizeOpenAIOAuthResponsesCompatibilityFields(req)) + }) + } + // Legacy prompt aliases and later-turn payloads use the same boundary. + body := []byte(`{"prompt":[{"role":"user","content":"hello","internal_chat_message_metadata_passthrough":{}}],"previous_response_id":"resp_123"}`) + out, changed, err := normalizeOpenAIOAuthResponsesCompatibilityBody(body) + require.NoError(t, err) + require.True(t, changed) + require.False(t, gjson.GetBytes(out, "input.0.internal_chat_message_metadata_passthrough").Exists()) + require.Equal(t, "resp_123", gjson.GetBytes(out, "previous_response_id").String()) + var req map[string]any + require.NoError(t, json.Unmarshal(body, &req)) + require.True(t, normalizeOpenAIOAuthResponsesCompatibilityFields(req)) + require.NotContains(t, req["input"].([]any)[0], "internal_chat_message_metadata_passthrough") +}