| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- package service
- import (
- "context"
- "encoding/json"
- "fmt"
- "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 TestAstraForwardPreservesMaxInPayloadAndUsage(t *testing.T) {
- for _, requestedModel := range []string{"gpt-6-astra", "astra-public"} {
- for _, stream := range []bool{false, true} {
- t.Run(fmt.Sprintf("%s/stream=%t", requestedModel, stream), func(t *testing.T) {
- response := `{"id":"resp_astra","model":"gpt-6-astra","status":"completed","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":1,"output_tokens":2}}`
- contentType := "application/json"
- if stream {
- response = "data: {\"type\":\"response.completed\",\"response\":" + response + "}\n\ndata: [DONE]\n\n"
- contentType = "text/event-stream"
- }
- upstream := &httpUpstreamRecorder{resp: &http.Response{StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{contentType}}, Body: io.NopCloser(strings.NewReader(response))}}
- svc := &OpenAIGatewayService{cfg: &config.Config{}, httpUpstream: upstream}
- account := rawGPT56ResponsesAPIKeyAccount(requestedModel, "gpt-6-astra")
- body, err := json.Marshal(map[string]any{"model": requestedModel, "stream": stream, "input": "hello", "reasoning": map[string]string{"effort": "max"}})
- require.NoError(t, err)
- c, _ := gin.CreateTestContext(httptest.NewRecorder())
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(string(body)))
- SetOpenAIClientTransport(c, OpenAIClientTransportHTTP)
- result, err := svc.Forward(context.Background(), c, account, body)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, "gpt-6-astra", 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)
- usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
- billingRepo := &openAIRecordUsageBillingRepoStub{result: &UsageBillingApplyResult{Applied: true}}
- usageService := newOpenAIRecordUsageServiceWithBillingRepoForTest(usageRepo, billingRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}, nil)
- err = usageService.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
- Result: result, Account: account, User: &User{ID: 21}, APIKey: &APIKey{ID: 22, Group: &Group{RateMultiplier: 1}},
- })
- require.NoError(t, err)
- require.NotNil(t, usageRepo.lastLog)
- require.Equal(t, "max", *usageRepo.lastLog.ReasoningEffort)
- require.Equal(t, "max", *usageRepo.lastLog.RequestedReasoningEffort)
- })
- }
- }
- }
- func TestNormalizeOpenAIReasoningEffortForMaxCapableModels(t *testing.T) {
- tests := []struct {
- name string
- raw string
- model string
- want string
- }{
- {name: "Astra 保留 max", raw: "max", model: "gpt-6-astra", want: "max"},
- {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: "DeepSeek V4 保留 max", raw: "max", model: "deepseek-v4-pro", want: "max"},
- {name: "旧 GPT 模型沿用 xhigh", raw: "max", model: "gpt-5.5", 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)
- }
|