Merge pull request #7038 from zenggang/contribute/gpt55-lite-compat

fix(openai): remove Responses Lite markers after OAuth mapping to GPT-5.5
This commit is contained in:
Wesley Liddick
2026-09-22 22:00:51 +08:00
committed by GitHub
5 changed files with 145 additions and 0 deletions
@@ -1523,6 +1523,9 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin.
setOpenAICodexRoutingHintFromBody(req.Header, account, body)
logOpenAIRoutingDiagnosticsFromBody(ctx, account, "http", req.Header, body, "not_applicable")
if err := applyMappedGPT55LiteCompatibility(req, account, body); err != nil {
return nil, err
}
return req, nil
}
@@ -732,6 +732,9 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough(
setOpenAICodexRoutingHintFromBody(req.Header, account, body)
logOpenAIRoutingDiagnosticsFromBody(ctx, account, "http_passthrough", req.Header, body, "not_applicable")
if err := applyMappedGPT55LiteCompatibility(req, account, body); err != nil {
return nil, err
}
return req, nil
}
@@ -0,0 +1,42 @@
package service
import (
"bytes"
"fmt"
"io"
"net/http"
"strings"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// The account mapping changes the model, not the upstream capability header.
// Apply at the final HTTP request boundary, after account/model resolution.
// Never mutate the ingress headers/body: failover may select a native account.
func applyMappedGPT55LiteCompatibility(req *http.Request, account *Account, body []byte) error {
if req == nil || account == nil || !account.IsOpenAIOAuthLike() {
return nil
}
if strings.TrimSpace(gjson.GetBytes(body, "model").String()) != "gpt-5.5" {
return nil
}
if !isOpenAIResponsesLiteHeader(req.Header.Get(responsesLiteHeader)) && !isOpenAIResponsesLiteWebSocketPayload(body) {
return nil
}
// The Codex non-Lite endpoint accepts additional_tools, namespaces and
// reasoning.context=all_turns. Preserve them and all history/tool results.
if isOpenAIResponsesLiteWebSocketPayload(body) {
var err error
body, err = sjson.DeleteBytes(body, "client_metadata."+responsesLiteWSMetadataKey)
if err != nil {
return fmt.Errorf("remove mapped GPT-5.5 Lite metadata: %w", err)
}
req.Body = io.NopCloser(bytes.NewReader(body))
req.ContentLength = int64(len(body))
savedBody := append([]byte(nil), body...)
req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(savedBody)), nil }
}
req.Header.Del(responsesLiteHeader)
return nil
}
@@ -0,0 +1,94 @@
package service
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestMappedGPT55LiteCompatibility(t *testing.T) {
for _, tc := range []struct {
name string
id int64
model string
mapped bool
wantLite bool
}{
{"mapped", 12, "gpt-5.5", true, false},
{"account13Mapped", 13, "gpt-5.5", true, false},
{"account14Mapped", 14, "gpt-5.5", true, false},
{"account13Native", 13, "gpt-6-astra", false, true},
{"account14Native", 14, "gpt-6-astra", false, true},
{"nativeSol", 12, "gpt-5.6-sol", true, true},
{"noMapping", 12, "gpt-5.5", false, false},
} {
t.Run(tc.name, func(t *testing.T) {
a := &Account{ID: tc.id, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Credentials: map[string]any{}}
if tc.mapped {
a.Credentials["model_mapping"] = map[string]any{"gpt-5.6-sol": "gpt-5.5"}
}
body := []byte(`{"model":"` + tc.model + `","input":[{"type":"additional_tools","role":"developer","tools":[]},{"type":"function_call_output","call_id":"call1","output":"12345678901234567890"}],"reasoning":{"context":"all_turns"},"client_metadata":{"ws_request_header_x_openai_internal_codex_responses_lite":"true","keep":"yes"}}`)
r, err := http.NewRequest("POST", "http://localhost", bytes.NewReader(body))
require.NoError(t, err)
r.Header.Set(responsesLiteHeader, "true")
require.NoError(t, applyMappedGPT55LiteCompatibility(r, a, body))
require.Equal(t, tc.wantLite, isOpenAIResponsesLiteHeader(r.Header.Get(responsesLiteHeader)))
got, err := io.ReadAll(r.Body)
require.NoError(t, err)
if tc.wantLite {
require.Equal(t, body, got)
} else {
require.False(t, isOpenAIResponsesLiteWebSocketPayload(got))
require.Equal(t, gjson.GetBytes(body, "input").Raw, gjson.GetBytes(got, "input").Raw)
require.Equal(t, "all_turns", gjson.GetBytes(got, "reasoning.context").String())
require.Equal(t, "yes", gjson.GetBytes(got, "client_metadata.keep").String())
require.Equal(t, int64(len(got)), r.ContentLength)
replay, err := r.GetBody()
require.NoError(t, err)
again, err := io.ReadAll(replay)
require.NoError(t, err)
require.Equal(t, got, again)
}
require.True(t, isOpenAIResponsesLiteWebSocketPayload(body))
})
}
}
func TestMappedGPT55LiteBuildersPreserveIngressForFailover(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, passthrough := range []bool{false, true} {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest("POST", "/v1/responses", nil)
c.Request.Header.Set(responsesLiteHeader, "true")
s := &OpenAIGatewayService{cfg: &config.Config{}}
a := &Account{ID: 12, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Credentials: map[string]any{"model_mapping": map[string]any{"gpt-5.6-sol": "gpt-5.5"}}}
body := []byte(`{"model":"gpt-5.5","stream":true,"input":[]}`)
var r *http.Request
var err error
if passthrough {
r, err = s.buildUpstreamRequestOpenAIPassthrough(context.Background(), c, a, body, "test-token")
} else {
r, err = s.buildUpstreamRequest(context.Background(), c, a, body, "test-token", true, "", true)
}
require.NoError(t, err)
require.Empty(t, r.Header.Get(responsesLiteHeader))
require.Equal(t, "true", c.GetHeader(responsesLiteHeader))
a.ID = 14
body = []byte(`{"model":"gpt-6-astra","stream":true,"input":[]}`)
if passthrough {
r, err = s.buildUpstreamRequestOpenAIPassthrough(context.Background(), c, a, body, "test-token")
} else {
r, err = s.buildUpstreamRequest(context.Background(), c, a, body, "test-token", true, "", true)
}
require.NoError(t, err)
require.Equal(t, "true", r.Header.Get(responsesLiteHeader))
}
}
@@ -505,6 +505,9 @@ func (s *OpenAIGatewayService) proxyOpenAIWSHTTPBridgeTurn(
if account.Platform != PlatformGrok && isOpenAIResponsesLiteWebSocketPayload(payload) {
upstreamReq.Header.Set(responsesLiteHeader, "true")
}
if err := applyMappedGPT55LiteCompatibility(upstreamReq, account, requestBody); err != nil {
return nil, err
}
return upstreamReq, nil
}
if account.Platform == PlatformGrok {