| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- package handler
- import (
- "bytes"
- "net/http"
- "net/http/httptest"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- "github.com/tidwall/gjson"
- "go.uber.org/zap"
- )
- func newCompactBodySignalTestContext(t *testing.T, path string, body []byte) *gin.Context {
- t.Helper()
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, path, bytes.NewReader(body))
- c.Request.Header.Set("Content-Type", "application/json")
- return c
- }
- func TestNormalizeOpenAIResponsesCompactRequest_RemoteV2StaysOnResponses(t *testing.T) {
- h := &OpenAIGatewayHandler{}
- tests := []struct {
- name string
- betaHeader string
- userAgent string
- }{
- {name: "headerless"},
- {name: "unrelated_header", betaHeader: "responses_websockets_v2"},
- {name: "wrong_case_header", betaHeader: "REMOTE_COMPACTION_V2"},
- {name: "declared_header", betaHeader: "remote_compaction_v2"},
- {name: "codex_cli_user_agent", userAgent: "codex_cli_rs/0.144.1 (Ubuntu 22.4.0; x86_64) xterm-256color"},
- {name: "codex_desktop_user_agent", userAgent: "Codex Desktop/0.139.0 (Mac OS X 14; arm64) unknown"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- body := []byte(`{
- "model":"gpt-5.6-sol",
- "stream":true,
- "store":true,
- "prompt_cache_key":"pck-signal-1",
- "reasoning":{"effort":"max","context":"all_turns"},
- "input":[
- {"type":"message","role":"user","content":"hello"},
- {"type":"compaction_trigger"}
- ]
- }`)
- c := newCompactBodySignalTestContext(t, "/v1/responses", body)
- if tt.betaHeader != "" {
- c.Request.Header.Set("x-codex-beta-features", tt.betaHeader)
- }
- if tt.userAgent != "" {
- c.Request.Header.Set("User-Agent", tt.userAgent)
- }
- normalized, ok := h.normalizeOpenAIResponsesCompactRequest(c, zap.NewNop(), body)
- require.True(t, ok)
- require.Equal(t, "/v1/responses", c.Request.URL.Path)
- require.False(t, isOpenAILegacyCompactPath(c))
- require.Equal(t, body, normalized)
- require.True(t, gjson.GetBytes(normalized, "stream").Bool())
- require.True(t, gjson.GetBytes(normalized, "store").Bool())
- require.Equal(t, "pck-signal-1", gjson.GetBytes(normalized, "prompt_cache_key").String())
- require.Equal(t, "max", gjson.GetBytes(normalized, "reasoning.effort").String())
- require.Equal(t, "all_turns", gjson.GetBytes(normalized, "reasoning.context").String())
- legacyCompact := service.IsOpenAIResponsesCompactPath(c)
- nativeV2 := isBareOpenAIResponsesPath(c) && isOpenAIRemoteCompactionV2Request(normalized)
- require.False(t, legacyCompact)
- require.True(t, nativeV2)
- require.Equal(t, service.OpenAIEndpointCapabilityResponses,
- openAIResponsesRequiredCapabilityForRequest(false, nativeV2 || legacyCompact, service.PlatformOpenAI))
- reqStream, streamOK := parseOpenAICompatibleStream(normalized)
- require.True(t, streamOK)
- require.True(t, reqStream)
- _, seedExists := c.Get(service.OpenAICompactSessionSeedKeyForTest())
- require.False(t, seedExists)
- _, streamMarkerExists := c.Get(service.OpenAICompactClientStreamKeyForTest())
- require.False(t, streamMarkerExists)
- })
- }
- }
- func TestNormalizeOpenAIResponsesCompactRequest_RemoteV2PathAliasesStayOnResponses(t *testing.T) {
- h := &OpenAIGatewayHandler{}
- body := []byte(`{"model":"gpt-5.6-sol","stream":true,"input":[{"type":"compaction_trigger"}]}`)
- for _, path := range []string{
- "/v1/responses/",
- "/openai/v1/responses",
- "/responses",
- "/backend-api/codex/responses",
- } {
- t.Run(path, func(t *testing.T) {
- c := newCompactBodySignalTestContext(t, path, body)
- normalized, ok := h.normalizeOpenAIResponsesCompactRequest(c, zap.NewNop(), body)
- require.True(t, ok)
- require.Equal(t, path, c.Request.URL.Path)
- require.Equal(t, body, normalized)
- legacyCompact := service.IsOpenAIResponsesCompactPath(c)
- nativeV2 := isBareOpenAIResponsesPath(c) && isOpenAIRemoteCompactionV2Request(normalized)
- require.False(t, legacyCompact)
- require.True(t, nativeV2)
- require.Equal(t, service.OpenAIEndpointCapabilityResponses,
- openAIResponsesRequiredCapabilityForRequest(false, nativeV2 || legacyCompact, service.PlatformOpenAI))
- })
- }
- }
- func TestOpenAIResponsesCompactionRoutingFlags(t *testing.T) {
- h := &OpenAIGatewayHandler{}
- tests := []struct {
- name string
- body []byte
- path string
- wantLegacyBefore bool
- wantNativeBefore bool
- wantLegacyAfter bool
- wantNativeAfter bool
- wantCapabilityAfter service.OpenAIEndpointCapability
- wantPathAfter string
- wantBodyUnchanged bool
- }{
- {
- name: "native_v2_stream_trigger",
- body: []byte(`{"model":"gpt-5.6-sol","stream":true,"input":[{"type":"compaction_trigger"}]}`),
- path: "/v1/responses",
- wantLegacyBefore: false,
- wantNativeBefore: true,
- wantLegacyAfter: false,
- wantNativeAfter: true,
- wantCapabilityAfter: service.OpenAIEndpointCapabilityResponses,
- wantPathAfter: "/v1/responses",
- wantBodyUnchanged: true,
- },
- {
- name: "native_v2_without_trigger",
- body: []byte(`{"model":"gpt-5.6-sol","stream":true,"input":[{"type":"message","role":"user","content":"hello"}]}`),
- path: "/v1/responses",
- wantLegacyBefore: false,
- wantNativeBefore: false,
- wantLegacyAfter: false,
- wantNativeAfter: false,
- wantCapabilityAfter: service.OpenAIEndpointCapabilityChatCompletions,
- wantPathAfter: "/v1/responses",
- wantBodyUnchanged: true,
- },
- {
- name: "explicit_compact",
- body: []byte(`{"model":"gpt-5.6-sol","stream":true,"input":[{"type":"compaction_trigger"}]}`),
- path: "/v1/responses/compact",
- wantLegacyBefore: true,
- wantNativeBefore: false,
- wantLegacyAfter: true,
- wantNativeAfter: false,
- wantCapabilityAfter: service.OpenAIEndpointCapabilityResponses,
- wantPathAfter: "/v1/responses/compact",
- },
- {
- name: "nested_compact",
- body: []byte(`{"model":"gpt-5.6-sol","stream":true,"input":[{"type":"compaction_trigger"}]}`),
- path: "/v1/responses/compact/detail",
- wantLegacyBefore: true,
- wantNativeBefore: false,
- wantLegacyAfter: true,
- wantNativeAfter: false,
- wantCapabilityAfter: service.OpenAIEndpointCapabilityResponses,
- wantPathAfter: "/v1/responses/compact/detail",
- },
- {
- name: "responses_subpath_with_native_signal",
- body: []byte(`{"model":"gpt-5.6-sol","stream":true,"input":[{"type":"compaction_trigger"}]}`),
- path: "/v1/responses/resp_123/responses",
- wantLegacyBefore: false,
- wantNativeBefore: false,
- wantLegacyAfter: false,
- wantNativeAfter: false,
- wantCapabilityAfter: service.OpenAIEndpointCapabilityChatCompletions,
- wantPathAfter: "/v1/responses/resp_123/responses",
- wantBodyUnchanged: true,
- },
- {
- name: "stream_false_promotes",
- body: []byte(`{"model":"gpt-5.6-sol","stream":false,"input":[{"type":"compaction_trigger"}]}`),
- path: "/v1/responses",
- wantLegacyBefore: false,
- wantNativeBefore: false,
- wantLegacyAfter: true,
- wantNativeAfter: false,
- wantCapabilityAfter: service.OpenAIEndpointCapabilityResponses,
- wantPathAfter: "/v1/responses/compact",
- },
- {
- name: "stream_absent_promotes",
- body: []byte(`{"model":"gpt-5.6-sol","input":[{"type":"compaction_trigger"}]}`),
- path: "/v1/responses",
- wantLegacyBefore: false,
- wantNativeBefore: false,
- wantLegacyAfter: true,
- wantNativeAfter: false,
- wantCapabilityAfter: service.OpenAIEndpointCapabilityResponses,
- wantPathAfter: "/v1/responses/compact",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- c := newCompactBodySignalTestContext(t, tt.path, tt.body)
- legacyBefore := service.IsOpenAIResponsesCompactPath(c)
- nativeBefore := isBareOpenAIResponsesPath(c) && isOpenAIRemoteCompactionV2Request(tt.body)
- require.Equal(t, tt.wantLegacyBefore, legacyBefore)
- require.Equal(t, tt.wantNativeBefore, nativeBefore)
- normalized, ok := h.normalizeOpenAIResponsesCompactRequest(c, zap.NewNop(), tt.body)
- require.True(t, ok)
- require.Equal(t, tt.wantPathAfter, c.Request.URL.Path)
- legacyAfter := service.IsOpenAIResponsesCompactPath(c)
- nativeAfter := isBareOpenAIResponsesPath(c) && isOpenAIRemoteCompactionV2Request(normalized)
- require.Equal(t, tt.wantLegacyAfter, legacyAfter)
- require.Equal(t, tt.wantNativeAfter, nativeAfter)
- require.Equal(t, tt.wantCapabilityAfter,
- openAIResponsesRequiredCapabilityForRequest(false, nativeAfter || legacyAfter, service.PlatformOpenAI))
- if tt.wantBodyUnchanged {
- require.Equal(t, tt.body, normalized)
- }
- })
- }
- }
- func TestNormalizeOpenAIResponsesCompactRequest_BodySignalTrailingSlashPromoted(t *testing.T) {
- h := &OpenAIGatewayHandler{}
- body := []byte(`{"model":"gpt-5.5","input":[{"type":"compaction_trigger"}]}`)
- c := newCompactBodySignalTestContext(t, "/v1/responses/", body)
- _, ok := h.normalizeOpenAIResponsesCompactRequest(c, zap.NewNop(), body)
- require.True(t, ok)
- require.Equal(t, "/v1/responses/compact", c.Request.URL.Path)
- }
- func TestNormalizeOpenAIResponsesCompactRequest_CodexDirectAliasPromoted(t *testing.T) {
- h := &OpenAIGatewayHandler{}
- body := []byte(`{"model":"gpt-5.5","input":[{"type":"compaction_trigger"}]}`)
- c := newCompactBodySignalTestContext(t, "/backend-api/codex/responses", body)
- _, ok := h.normalizeOpenAIResponsesCompactRequest(c, zap.NewNop(), body)
- require.True(t, ok)
- require.Equal(t, "/backend-api/codex/responses/compact", c.Request.URL.Path)
- }
- func TestNormalizeOpenAIResponsesCompactRequest_NonRemoteV2BodySignalPromoted(t *testing.T) {
- h := &OpenAIGatewayHandler{}
- tests := []struct {
- name string
- body []byte
- betaHeader string
- wantMarked bool
- }{
- {
- name: "stream_false_headerless",
- body: []byte(`{"model":"gpt-5.5","stream":false,"input":[{"type":"compaction_trigger"}]}`),
- },
- {
- name: "stream_absent_headerless",
- body: []byte(`{"model":"gpt-5.5","input":[{"type":"compaction_trigger"}]}`),
- },
- {
- name: "stream_false_declared_header",
- body: []byte(`{"model":"gpt-5.5","stream":false,"input":[{"type":"compaction_trigger"}]}`),
- betaHeader: "remote_compaction_v2",
- },
- {
- name: "stream_absent_wrong_case_header",
- body: []byte(`{"model":"gpt-5.5","input":[{"type":"compaction_trigger"}]}`),
- betaHeader: "REMOTE_COMPACTION_V2",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- c := newCompactBodySignalTestContext(t, "/v1/responses", tt.body)
- if tt.betaHeader != "" {
- c.Request.Header.Set("x-codex-beta-features", tt.betaHeader)
- }
- normalized, ok := h.normalizeOpenAIResponsesCompactRequest(c, zap.NewNop(), tt.body)
- require.True(t, ok)
- require.Equal(t, "/v1/responses/compact", c.Request.URL.Path)
- require.False(t, gjson.GetBytes(normalized, "stream").Exists())
- marked, exists := c.Get(service.OpenAICompactClientStreamKeyForTest())
- require.Equal(t, tt.wantMarked, exists)
- if tt.wantMarked {
- require.Equal(t, true, marked)
- }
- })
- }
- }
- func TestNormalizeOpenAIResponsesCompactRequest_NoTriggerUntouched(t *testing.T) {
- h := &OpenAIGatewayHandler{}
- body := []byte(`{"model":"gpt-5.5","stream":true,"input":[{"type":"message","role":"user","content":"hello"}]}`)
- c := newCompactBodySignalTestContext(t, "/v1/responses", body)
- normalized, ok := h.normalizeOpenAIResponsesCompactRequest(c, zap.NewNop(), body)
- require.True(t, ok)
- require.Equal(t, "/v1/responses", c.Request.URL.Path)
- require.False(t, isOpenAILegacyCompactPath(c))
- require.Equal(t, body, normalized)
- require.True(t, gjson.GetBytes(normalized, "stream").Bool())
- }
- func TestNormalizeOpenAIResponsesCompactRequest_PathBasedNoDoubleSuffix(t *testing.T) {
- h := &OpenAIGatewayHandler{}
- body := []byte(`{"model":"gpt-5.5","stream":true,"store":true,"input":[{"type":"message","role":"user","content":"hello"}]}`)
- c := newCompactBodySignalTestContext(t, "/v1/responses/compact", body)
- normalized, ok := h.normalizeOpenAIResponsesCompactRequest(c, zap.NewNop(), body)
- require.True(t, ok)
- require.Equal(t, "/v1/responses/compact", c.Request.URL.Path)
- require.False(t, gjson.GetBytes(normalized, "stream").Exists())
- require.False(t, gjson.GetBytes(normalized, "store").Exists())
- }
- func TestNormalizeOpenAIResponsesCompactRequest_SubpathNotPromoted(t *testing.T) {
- h := &OpenAIGatewayHandler{}
- body := []byte(`{"model":"gpt-5.5","input":[{"type":"compaction_trigger"}]}`)
- c := newCompactBodySignalTestContext(t, "/v1/responses/resp_123/cancel", body)
- normalized, ok := h.normalizeOpenAIResponsesCompactRequest(c, zap.NewNop(), body)
- require.True(t, ok)
- require.Equal(t, "/v1/responses/resp_123/cancel", c.Request.URL.Path)
- require.Equal(t, body, normalized)
- }
- // path-based compact(Codex v1 unary 协议)即使 body 带 stream:true 也不标记,
- // 保持 JSON 写回行为不变。
- func TestNormalizeOpenAIResponsesCompactRequest_PathBasedStreamTrueNotMarked(t *testing.T) {
- h := &OpenAIGatewayHandler{}
- body := []byte(`{"model":"gpt-5.5","stream":true,"input":[{"type":"message","role":"user","content":"hello"}]}`)
- c := newCompactBodySignalTestContext(t, "/v1/responses/compact", body)
- _, ok := h.normalizeOpenAIResponsesCompactRequest(c, zap.NewNop(), body)
- require.True(t, ok)
- _, exists := c.Get(service.OpenAICompactClientStreamKeyForTest())
- require.False(t, exists)
- }
|