package service import ( "bytes" "context" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" ) func TestNormalizeOpenAICompatRequestedModel(t *testing.T) { t.Parallel() tests := []struct { name string input string want string }{ {name: "gpt reasoning alias strips xhigh", input: "gpt-5.4-xhigh", want: "gpt-5.4"}, {name: "gpt reasoning alias strips none", input: "gpt-5.4-none", want: "gpt-5.4"}, {name: "codex max model stays intact", input: "gpt-5.1-codex-max", want: "gpt-5.1-codex-max"}, {name: "non openai model unchanged", input: "claude-opus-4-6", want: "claude-opus-4-6"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.want, NormalizeOpenAICompatRequestedModel(tt.input)) }) } } func TestApplyOpenAICompatModelNormalization(t *testing.T) { t.Parallel() t.Run("derives xhigh from model suffix when output config missing", func(t *testing.T) { req := &apicompat.AnthropicRequest{Model: "gpt-5.4-xhigh"} applyOpenAICompatModelNormalization(req) require.Equal(t, "gpt-5.4", req.Model) require.NotNil(t, req.OutputConfig) require.Equal(t, "max", req.OutputConfig.Effort) }) t.Run("explicit output config wins over model suffix", func(t *testing.T) { req := &apicompat.AnthropicRequest{ Model: "gpt-5.4-xhigh", OutputConfig: &apicompat.AnthropicOutputConfig{Effort: "low"}, } applyOpenAICompatModelNormalization(req) require.Equal(t, "gpt-5.4", req.Model) require.NotNil(t, req.OutputConfig) require.Equal(t, "low", req.OutputConfig.Effort) }) t.Run("non openai model is untouched", func(t *testing.T) { req := &apicompat.AnthropicRequest{Model: "claude-opus-4-6"} applyOpenAICompatModelNormalization(req) require.Equal(t, "claude-opus-4-6", req.Model) require.Nil(t, req.OutputConfig) }) } func TestForwardAsAnthropic_NormalizesRoutingAndEffortForGpt54XHigh(t *testing.T) { t.Parallel() gin.SetMode(gin.TestMode) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) body := []byte(`{"model":"gpt-5.4-xhigh","max_tokens":16,"messages":[{"role":"user","content":"hello"}],"stream":false}`) c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body)) c.Request.Header.Set("Content-Type", "application/json") upstreamBody := strings.Join([]string{ `data: {"type":"response.completed","response":{"id":"resp_1","object":"response","model":"gpt-5.4","status":"completed","output":[{"type":"message","id":"msg_1","role":"assistant","status":"completed","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":5,"output_tokens":2,"total_tokens":7}}}`, "", "data: [DONE]", "", }, "\n") upstream := &httpUpstreamRecorder{resp: &http.Response{ StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_compat"}}, Body: io.NopCloser(strings.NewReader(upstreamBody)), }} svc := &OpenAIGatewayService{httpUpstream: upstream} account := &Account{ ID: 1, Name: "openai-oauth", Platform: PlatformOpenAI, Type: AccountTypeOAuth, Concurrency: 1, Credentials: map[string]any{ "access_token": "oauth-token", "chatgpt_account_id": "chatgpt-acc", "model_mapping": map[string]any{ "gpt-5.4": "gpt-5.4", }, }, } result, err := svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "gpt-5.1") require.NoError(t, err) require.NotNil(t, result) require.Equal(t, "gpt-5.4-xhigh", result.Model) require.Equal(t, "gpt-5.4", result.UpstreamModel) require.Equal(t, "gpt-5.4", result.BillingModel) require.NotNil(t, result.ReasoningEffort) require.Equal(t, "xhigh", *result.ReasoningEffort) require.Equal(t, "gpt-5.4", gjson.GetBytes(upstream.lastBody, "model").String()) require.Equal(t, "xhigh", gjson.GetBytes(upstream.lastBody, "reasoning.effort").String()) require.Equal(t, http.StatusOK, rec.Code) require.Equal(t, "gpt-5.4-xhigh", gjson.GetBytes(rec.Body.Bytes(), "model").String()) require.Equal(t, "ok", gjson.GetBytes(rec.Body.Bytes(), "content.0.text").String()) t.Logf("upstream body: %s", string(upstream.lastBody)) t.Logf("response body: %s", rec.Body.String()) }