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
123 lines
3.7 KiB
Go
123 lines
3.7 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 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)
|
|
}
|