openai_reasoning_effort_candidates_test.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. package service
  2. import (
  3. "context"
  4. "io"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. "github.com/gin-gonic/gin"
  11. "github.com/stretchr/testify/require"
  12. "github.com/tidwall/gjson"
  13. )
  14. func TestExtractOpenAIReasoningEffortFromBodyModelCandidates(t *testing.T) {
  15. bodyWithoutEffort := []byte(`{"model":"whatever","input":"hello"}`)
  16. bodyWithMax := []byte(`{"model":"sol","reasoning":{"effort":"max"},"input":"hello"}`)
  17. tests := []struct {
  18. name string
  19. body []byte
  20. candidates []string
  21. want string // "" 表示期望 nil
  22. }{
  23. {
  24. name: "后缀推导回退到原始模型(OAuth 上游模型已剥后缀)",
  25. body: bodyWithoutEffort,
  26. candidates: []string{"gpt-5.4", "gpt-5.4", "gpt-5.4-xhigh"},
  27. want: "xhigh",
  28. },
  29. {
  30. name: "GPT-5.6 后缀 max 经原始模型推导保留",
  31. body: bodyWithoutEffort,
  32. candidates: []string{"gpt-5.6-sol", "gpt-5.6-sol", "gpt-5.6-sol-max"},
  33. want: "max",
  34. },
  35. {
  36. name: "显式 max 用第一个非空候选(映射后模型)判定",
  37. body: bodyWithMax,
  38. candidates: []string{"gpt-5.6-sol", "sol"},
  39. want: "max",
  40. },
  41. {
  42. name: "显式 max 非 5.6 首候选仍折叠为 xhigh",
  43. body: bodyWithMax,
  44. candidates: []string{"gpt-5.4", "sol"},
  45. want: "xhigh",
  46. },
  47. {
  48. name: "所有候选均无后缀时返回 nil",
  49. body: bodyWithoutEffort,
  50. candidates: []string{"gpt-5.4", "gpt-5.4", "gpt-5.4"},
  51. want: "",
  52. },
  53. }
  54. for _, tt := range tests {
  55. t.Run(tt.name, func(t *testing.T) {
  56. got := extractOpenAIReasoningEffortFromBody(tt.body, tt.candidates...)
  57. if tt.want == "" {
  58. require.Nil(t, got)
  59. return
  60. }
  61. require.NotNil(t, got)
  62. require.Equal(t, tt.want, *got)
  63. })
  64. }
  65. }
  66. func TestExtractOpenAIReasoningEffortModelCandidates(t *testing.T) {
  67. reqBody := map[string]any{"model": "gpt-5.3-codex-high", "input": "hello"}
  68. got := extractOpenAIReasoningEffort(reqBody, "gpt-5.3-codex", "gpt-5.3-codex-high")
  69. require.NotNil(t, got)
  70. require.Equal(t, "high", *got)
  71. }
  72. // 回归:OAuth 账号请求后缀式模型(无显式 reasoning 字段)时,上游模型被
  73. // normalizeCodexModel 剥掉 effort 后缀,用量元数据的 effort 必须仍能从
  74. // 原始模型名后缀推导出来。
  75. func TestOpenAIGatewayServiceForwardOAuthDerivesEffortFromSuffixModel(t *testing.T) {
  76. gin.SetMode(gin.TestMode)
  77. upstream := &httpUpstreamRecorder{
  78. resp: &http.Response{
  79. StatusCode: http.StatusOK,
  80. Header: http.Header{"Content-Type": []string{"application/json"}},
  81. Body: io.NopCloser(strings.NewReader(`{"usage":{"input_tokens":1,"output_tokens":2}}`)),
  82. },
  83. }
  84. cfg := &config.Config{}
  85. cfg.Security.URLAllowlist.Enabled = false
  86. svc := &OpenAIGatewayService{cfg: cfg, httpUpstream: upstream}
  87. account := &Account{
  88. ID: 11,
  89. Name: "openai-oauth-suffix",
  90. Platform: PlatformOpenAI,
  91. Type: AccountTypeOAuth,
  92. Concurrency: 1,
  93. Credentials: map[string]any{
  94. "access_token": "oauth-token",
  95. "chatgpt_account_id": "chatgpt-acc",
  96. },
  97. Status: StatusActive,
  98. Schedulable: true,
  99. }
  100. rec := httptest.NewRecorder()
  101. c, _ := gin.CreateTestContext(rec)
  102. c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", nil)
  103. SetOpenAIClientTransport(c, OpenAIClientTransportHTTP)
  104. body := []byte(`{"model":"gpt-5.3-codex-xhigh","instructions":"suffix-test","input":"hello","stream":false}`)
  105. result, err := svc.Forward(context.Background(), c, account, body)
  106. require.NoError(t, err)
  107. require.NotNil(t, result)
  108. require.Equal(t, "gpt-5.3-codex", gjson.GetBytes(upstream.lastBody, "model").String())
  109. require.NotNil(t, result.ReasoningEffort)
  110. require.Equal(t, "xhigh", *result.ReasoningEffort)
  111. }