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 TestExtractOpenAIReasoningEffortFromBodyModelCandidates(t *testing.T) { bodyWithoutEffort := []byte(`{"model":"whatever","input":"hello"}`) bodyWithMax := []byte(`{"model":"sol","reasoning":{"effort":"max"},"input":"hello"}`) tests := []struct { name string body []byte candidates []string want string // "" 表示期望 nil }{ { name: "后缀推导回退到原始模型(OAuth 上游模型已剥后缀)", body: bodyWithoutEffort, candidates: []string{"gpt-5.4", "gpt-5.4", "gpt-5.4-xhigh"}, want: "xhigh", }, { name: "GPT-5.6 后缀 max 经原始模型推导保留", body: bodyWithoutEffort, candidates: []string{"gpt-5.6-sol", "gpt-5.6-sol", "gpt-5.6-sol-max"}, want: "max", }, { name: "显式 max 用第一个非空候选(映射后模型)判定", body: bodyWithMax, candidates: []string{"gpt-5.6-sol", "sol"}, want: "max", }, { name: "显式 max 非 5.6 首候选仍折叠为 xhigh", body: bodyWithMax, candidates: []string{"gpt-5.4", "sol"}, want: "xhigh", }, { name: "所有候选均无后缀时返回 nil", body: bodyWithoutEffort, candidates: []string{"gpt-5.4", "gpt-5.4", "gpt-5.4"}, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := extractOpenAIReasoningEffortFromBody(tt.body, tt.candidates...) if tt.want == "" { require.Nil(t, got) return } require.NotNil(t, got) require.Equal(t, tt.want, *got) }) } } func TestExtractOpenAIReasoningEffortModelCandidates(t *testing.T) { reqBody := map[string]any{"model": "gpt-5.3-codex-high", "input": "hello"} got := extractOpenAIReasoningEffort(reqBody, "gpt-5.3-codex", "gpt-5.3-codex-high") require.NotNil(t, got) require.Equal(t, "high", *got) } // 回归:OAuth 账号请求后缀式模型(无显式 reasoning 字段)时,上游模型被 // normalizeCodexModel 剥掉 effort 后缀,用量元数据的 effort 必须仍能从 // 原始模型名后缀推导出来。 func TestOpenAIGatewayServiceForwardOAuthDerivesEffortFromSuffixModel(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: 11, Name: "openai-oauth-suffix", 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", nil) SetOpenAIClientTransport(c, OpenAIClientTransportHTTP) body := []byte(`{"model":"gpt-5.3-codex-xhigh","instructions":"suffix-test","input":"hello","stream":false}`) result, err := svc.Forward(context.Background(), c, account, body) require.NoError(t, err) require.NotNil(t, result) require.Equal(t, "gpt-5.3-codex", gjson.GetBytes(upstream.lastBody, "model").String()) require.NotNil(t, result.ReasoningEffort) require.Equal(t, "xhigh", *result.ReasoningEffort) }