Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
340 lines
13 KiB
Go
340 lines
13 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func TestNormalizeOpenAIReasoningEffortForGPT56(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw string
|
|
model string
|
|
want string
|
|
}{
|
|
{name: "Sol 保留 max", raw: "max", model: "gpt-5.6-sol", want: "max"},
|
|
{name: "Terra 保留 max", raw: "max", model: "openai/gpt-5.6-terra", want: "max"},
|
|
{name: "Luna 后缀保留 max", raw: "max", model: "gpt-5.6-luna-2026-07-09", want: "max"},
|
|
{name: "其他模型沿用 xhigh", raw: "max", model: "deepseek-v4-pro", want: "xhigh"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require.Equal(t, tt.want, normalizeOpenAIReasoningEffortForModel(tt.raw, tt.model))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeOpenAICodexCompactReasoningEffortDowngradesMax(t *testing.T) {
|
|
body := []byte(`{"model":"gpt-5.6-sol","input":"compact me","reasoning":{"effort":"max","summary":"auto"}}`)
|
|
|
|
normalized, changed, err := normalizeOpenAICodexCompactReasoningEffort(body, "gpt-5.6-sol")
|
|
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
require.Equal(t, "gpt-5.6-sol", gjson.GetBytes(normalized, "model").String())
|
|
require.Equal(t, "xhigh", gjson.GetBytes(normalized, "reasoning.effort").String())
|
|
require.Equal(t, "auto", gjson.GetBytes(normalized, "reasoning.summary").String())
|
|
}
|
|
|
|
func TestNormalizeOpenAICodexCompactReasoningEffortForAccountScopesCompatibility(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
body := []byte(`{"model":"gpt-5.6-sol","input":"compact me","reasoning":{"effort":"max"}}`)
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
account *Account
|
|
changed bool
|
|
want string
|
|
}{
|
|
{
|
|
name: "OpenAI OAuth compact 降级",
|
|
path: "/openai/v1/responses/compact",
|
|
account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth},
|
|
changed: true,
|
|
want: "xhigh",
|
|
},
|
|
{
|
|
name: "OpenAI OAuth 普通请求保留",
|
|
path: "/openai/v1/responses",
|
|
account: &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth},
|
|
want: "max",
|
|
},
|
|
{
|
|
name: "OpenAI API Key compact 保留",
|
|
path: "/openai/v1/responses/compact",
|
|
account: &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
|
|
want: "max",
|
|
},
|
|
{
|
|
name: "Grok OAuth compact 保留",
|
|
path: "/openai/v1/responses/compact",
|
|
account: &Account{Platform: PlatformGrok, Type: AccountTypeOAuth},
|
|
want: "max",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodPost, tt.path, nil)
|
|
|
|
normalized, changed, err := normalizeOpenAICodexCompactReasoningEffortForAccount(c, tt.account, body)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.changed, changed)
|
|
require.Equal(t, tt.want, gjson.GetBytes(normalized, "reasoning.effort").String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpenAIGatewayServiceForwardPreservesGPT56MaxEffort(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
upstream := &httpUpstreamRecorder{
|
|
resp: &http.Response{
|
|
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: 7,
|
|
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,"reasoning":{"effort":"max"},"input":"hello"}`)
|
|
result, err := svc.Forward(context.Background(), c, account, body)
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
require.Equal(t, "max", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String())
|
|
require.NotNil(t, result.ReasoningEffort)
|
|
require.Equal(t, "max", *result.ReasoningEffort)
|
|
}
|
|
|
|
func TestOpenAIGatewayServiceForwardPreservesMappedGPT56MaxEffort(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
upstream := &httpUpstreamRecorder{
|
|
resp: &http.Response{
|
|
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: 9,
|
|
Name: "openai-apikey-mapped",
|
|
Platform: PlatformOpenAI,
|
|
Type: AccountTypeAPIKey,
|
|
Concurrency: 1,
|
|
Credentials: map[string]any{
|
|
"api_key": "sk-test",
|
|
"base_url": "https://example.com",
|
|
"model_mapping": map[string]any{
|
|
"sol": "gpt-5.6-sol",
|
|
},
|
|
},
|
|
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":"sol","stream":false,"reasoning":{"effort":"max"},"input":"hello"}`)
|
|
result, err := svc.Forward(context.Background(), c, account, body)
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
require.Equal(t, "gpt-5.6-sol", gjson.GetBytes(upstream.lastBody, "model").String())
|
|
require.Equal(t, "max", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String())
|
|
require.NotNil(t, result.ReasoningEffort)
|
|
require.Equal(t, "max", *result.ReasoningEffort)
|
|
}
|
|
|
|
func TestOpenAIGatewayServiceForwardOAuthCompactDowngradesMaxEffort(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
upstream := &httpUpstreamRecorder{
|
|
resp: &http.Response{
|
|
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: 8,
|
|
Name: "openai-oauth",
|
|
Platform: PlatformOpenAI,
|
|
Type: AccountTypeOAuth,
|
|
Concurrency: 1,
|
|
Credentials: map[string]any{
|
|
"access_token": "oauth-token",
|
|
"chatgpt_account_id": "chatgpt-acc",
|
|
},
|
|
Status: StatusActive,
|
|
Schedulable: true,
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses/compact", nil)
|
|
SetOpenAIClientTransport(c, OpenAIClientTransportHTTP)
|
|
|
|
body := []byte(`{"model":"gpt-5.6-sol","instructions":"compact-test","input":"hello","reasoning":{"effort":"max"}}`)
|
|
result, err := svc.Forward(context.Background(), c, account, body)
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, upstream.lastReq)
|
|
require.Equal(t, chatgptCodexURL+"/compact", upstream.lastReq.URL.String())
|
|
require.Equal(t, "xhigh", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String())
|
|
require.NotNil(t, result.ReasoningEffort)
|
|
require.Equal(t, "xhigh", *result.ReasoningEffort)
|
|
}
|
|
|
|
func TestOpenAIGatewayServiceForwardOAuthRemoteCompactV2PreservesResponsesWire(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
upstream := &httpUpstreamRecorder{
|
|
resp: &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
|
|
Body: io.NopCloser(strings.NewReader(
|
|
"data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"compaction\",\"encrypted_content\":\"summary\"}}\n\n" +
|
|
"data: {\"type\":\"response.completed\",\"response\":{\"usage\":{\"input_tokens\":1,\"output_tokens\":2}}}\n\n" +
|
|
"data: [DONE]\n\n",
|
|
)),
|
|
},
|
|
}
|
|
cfg := &config.Config{}
|
|
cfg.Security.URLAllowlist.Enabled = false
|
|
svc := &OpenAIGatewayService{cfg: cfg, httpUpstream: upstream}
|
|
account := &Account{
|
|
ID: 10,
|
|
Name: "openai-oauth-responses",
|
|
Platform: PlatformOpenAI,
|
|
Type: AccountTypeOAuth,
|
|
Concurrency: 1,
|
|
Credentials: map[string]any{
|
|
"access_token": "oauth-token",
|
|
"chatgpt_account_id": "chatgpt-acc",
|
|
"compact_model_mapping": map[string]any{
|
|
"gpt-5.6-sol": "gpt-5.6-sol-openai-compact",
|
|
},
|
|
},
|
|
Status: StatusActive,
|
|
Schedulable: true,
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", nil)
|
|
c.Request.Header.Set("x-codex-beta-features", "remote_compaction_v2")
|
|
SetOpenAIClientTransport(c, OpenAIClientTransportHTTP)
|
|
|
|
body := []byte(`{"model":"gpt-5.6-sol","stream":true,"instructions":"response-test","input":[{"type":"message","role":"user","content":"hello"},{"type":"compaction_trigger"}],"reasoning":{"effort":"max","context":"all_turns"}}`)
|
|
result, err := svc.Forward(context.Background(), c, account, body)
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, upstream.lastReq)
|
|
require.Equal(t, chatgptCodexURL, upstream.lastReq.URL.String())
|
|
require.Equal(t, "gpt-5.6-sol", gjson.GetBytes(upstream.lastBody, "model").String())
|
|
require.True(t, gjson.GetBytes(upstream.lastBody, "stream").Bool())
|
|
require.Equal(t, "compaction_trigger", gjson.GetBytes(upstream.lastBody, "input.#(type==\"compaction_trigger\").type").String())
|
|
require.Equal(t, "max", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String())
|
|
require.Equal(t, "all_turns", gjson.GetBytes(upstream.lastBody, "reasoning.context").String())
|
|
require.Equal(t, "remote_compaction_v2", upstream.lastReq.Header.Get("x-codex-beta-features"))
|
|
require.Contains(t, rec.Body.String(), `"type":"compaction"`)
|
|
require.Contains(t, rec.Body.String(), `"encrypted_content":"summary"`)
|
|
require.NotNil(t, result.ReasoningEffort)
|
|
require.Equal(t, "max", *result.ReasoningEffort)
|
|
}
|
|
|
|
func TestOpenAIGatewayServiceForwardAPIKeyRemoteCompactV2PreservesResponsesWire(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
upstream := &httpUpstreamRecorder{
|
|
resp: &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
|
|
Body: io.NopCloser(strings.NewReader(
|
|
"data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"compaction\",\"encrypted_content\":\"summary\"}}\n\n" +
|
|
"data: {\"type\":\"response.completed\",\"response\":{\"usage\":{\"input_tokens\":1,\"output_tokens\":2}}}\n\n" +
|
|
"data: [DONE]\n\n",
|
|
)),
|
|
},
|
|
}
|
|
cfg := &config.Config{}
|
|
cfg.Security.URLAllowlist.Enabled = false
|
|
svc := &OpenAIGatewayService{cfg: cfg, httpUpstream: upstream}
|
|
account := &Account{
|
|
ID: 11,
|
|
Name: "openai-apikey-responses",
|
|
Platform: PlatformOpenAI,
|
|
Type: AccountTypeAPIKey,
|
|
Concurrency: 1,
|
|
Credentials: map[string]any{
|
|
"api_key": "sk-test",
|
|
"base_url": "https://example.com/v1",
|
|
"compact_model_mapping": map[string]any{
|
|
"gpt-5.6-sol": "gpt-5.6-sol-openai-compact",
|
|
},
|
|
},
|
|
Extra: map[string]any{"use_responses_api": true},
|
|
Status: StatusActive,
|
|
Schedulable: true,
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", nil)
|
|
c.Request.Header.Set("x-codex-beta-features", "remote_compaction_v2")
|
|
SetOpenAIClientTransport(c, OpenAIClientTransportHTTP)
|
|
|
|
body := []byte(`{"model":"gpt-5.6-sol","stream":true,"instructions":"response-test","input":[{"type":"message","role":"user","content":"hello"},{"type":"compaction_trigger"}],"reasoning":{"effort":"max","context":"all_turns"}}`)
|
|
result, err := svc.Forward(context.Background(), c, account, body)
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, upstream.lastReq)
|
|
require.Equal(t, "https://example.com/v1/responses", upstream.lastReq.URL.String())
|
|
require.Equal(t, "gpt-5.6-sol", gjson.GetBytes(upstream.lastBody, "model").String())
|
|
require.True(t, gjson.GetBytes(upstream.lastBody, "stream").Bool())
|
|
require.Equal(t, "compaction_trigger", gjson.GetBytes(upstream.lastBody, "input.#(type==\"compaction_trigger\").type").String())
|
|
require.Equal(t, "max", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String())
|
|
require.Equal(t, "all_turns", gjson.GetBytes(upstream.lastBody, "reasoning.context").String())
|
|
require.Equal(t, "remote_compaction_v2", upstream.lastReq.Header.Get("x-codex-beta-features"))
|
|
require.Contains(t, rec.Body.String(), `"type":"compaction"`)
|
|
require.Contains(t, rec.Body.String(), `"encrypted_content":"summary"`)
|
|
require.NotNil(t, result.ReasoningEffort)
|
|
require.Equal(t, "max", *result.ReasoningEffort)
|
|
}
|