| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- package service
- import (
- "context"
- "errors"
- "fmt"
- "net/http"
- "net/http/httptest"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- // HTTP POST /v1/responses -> forwardOpenAIWSV2 keeps the canonical outbound
- // tier separate from response.completed.service_tier for usage-time billing.
- func TestForwardOpenAIWSV2_KeepsOutboundAndObservedServiceTiersSeparate(t *testing.T) {
- gin.SetMode(gin.TestMode)
- cases := []struct {
- name string
- requestTier string
- stream bool
- }{
- {name: "priority_nonstream", requestTier: "priority", stream: false},
- {name: "fast_stream", requestTier: "fast", stream: true},
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
- c.Request.Header.Set("User-Agent", "unit-test-agent/1.0")
- cfg := &config.Config{}
- cfg.Security.URLAllowlist.Enabled = false
- cfg.Security.URLAllowlist.AllowInsecureHTTP = true
- cfg.Gateway.OpenAIWS.Enabled = true
- cfg.Gateway.OpenAIWS.APIKeyEnabled = true
- cfg.Gateway.OpenAIWS.ResponsesWebsocketsV2 = true
- cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 1
- cfg.Gateway.OpenAIWS.MinIdlePerAccount = 0
- cfg.Gateway.OpenAIWS.MaxIdlePerAccount = 1
- cfg.Gateway.OpenAIWS.QueueLimitPerConn = 8
- cfg.Gateway.OpenAIWS.DialTimeoutSeconds = 3
- cfg.Gateway.OpenAIWS.ReadTimeoutSeconds = 5
- cfg.Gateway.OpenAIWS.WriteTimeoutSeconds = 3
- captureConn := &openAIWSCaptureConn{
- events: [][]byte{
- []byte(`{"type":"response.completed","response":{"id":"resp_tier_v2","model":"gpt-5.5","status":"completed","service_tier":"default","usage":{"input_tokens":1,"output_tokens":1}}}`),
- },
- }
- captureDialer := &openAIWSCaptureDialer{conn: captureConn}
- pool := newOpenAIWSConnPool(cfg)
- pool.setClientDialerForTest(captureDialer)
- svc := &OpenAIGatewayService{
- cfg: cfg,
- httpUpstream: &httpUpstreamRecorder{},
- cache: &stubGatewayCache{},
- openaiWSResolver: NewOpenAIWSProtocolResolver(cfg),
- toolCorrector: NewCodexToolCorrector(),
- openaiWSPool: pool,
- }
- account := &Account{
- ID: 5882,
- Name: "openai-ws-v2-tier",
- Platform: PlatformOpenAI,
- Type: AccountTypeAPIKey,
- Status: StatusActive,
- Schedulable: true,
- Concurrency: 1,
- Credentials: map[string]any{"api_key": "sk-test"},
- Extra: map[string]any{"responses_websockets_v2_enabled": true},
- }
- body := []byte(fmt.Sprintf(
- `{"model":"gpt-5.5","stream":%t,"service_tier":%q,"input":[{"type":"input_text","text":"hi"}]}`,
- tc.stream, tc.requestTier,
- ))
- result, err := svc.Forward(context.Background(), c, account, body)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.True(t, result.OpenAIWSMode, "must take HTTP POST → forwardOpenAIWSV2, not HTTP fallback")
- require.Equal(t, tc.stream, result.Stream)
- require.Equal(t, "resp_tier_v2", result.RequestID)
- require.NotNil(t, result.ServiceTier)
- require.Equal(t, "priority", *result.ServiceTier)
- require.Equal(t, "default", result.UpstreamResponseServiceTier)
- require.Equal(t, "priority", captureConn.lastWrite["service_tier"],
- "outbound WS payload still carries the requested Fast tier")
- })
- }
- }
- func TestForwardOpenAIWSV2_MarksCyberPolicyForFailureEventShapes(t *testing.T) {
- gin.SetMode(gin.TestMode)
- tests := []struct {
- name string
- upstreamEvent []byte
- wantError bool
- wantInput int
- wantOutput int
- }{
- {
- name: "error_before_return",
- upstreamEvent: []byte(`{"type":"error","error":{"type":"rate_limit_error","code":"cyber_policy","message":"rate limit exceeded by cyber policy"},"usage":{"input_tokens":5,"output_tokens":1}}`),
- wantError: true,
- wantInput: 5,
- wantOutput: 1,
- },
- {
- name: "response_failed_terminal",
- upstreamEvent: []byte(`{"type":"response.failed","response":{"id":"resp_cyber","status":"failed","error":{"code":"cyber_policy","message":"blocked by cyber policy"},"usage":{"input_tokens":9,"output_tokens":2}}}`),
- wantInput: 9,
- wantOutput: 2,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(http.MethodPost, "/openai/v1/responses", nil)
- cfg := newOpenAIWSV2TestConfig()
- cfg.Security.URLAllowlist.Enabled = false
- cfg.Security.URLAllowlist.AllowInsecureHTTP = true
- cfg.Gateway.OpenAIWS.MinIdlePerAccount = 0
- captureConn := &openAIWSCaptureConn{events: [][]byte{append([]byte(nil), tt.upstreamEvent...)}}
- pool := newOpenAIWSConnPool(cfg)
- pool.setClientDialerForTest(&openAIWSCaptureDialer{conn: captureConn})
- svc := &OpenAIGatewayService{
- cfg: cfg,
- httpUpstream: &httpUpstreamRecorder{},
- cache: &stubGatewayCache{},
- openaiWSResolver: NewOpenAIWSProtocolResolver(cfg),
- toolCorrector: NewCodexToolCorrector(),
- openaiWSPool: pool,
- }
- account := &Account{
- ID: 5883, Name: "openai-ws-v2-cyber", Platform: PlatformOpenAI,
- Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 1,
- Credentials: map[string]any{"api_key": "sk-test"},
- Extra: map[string]any{"responses_websockets_v2_enabled": true},
- }
- result, err := svc.Forward(context.Background(), c, account, []byte(`{"model":"gpt-5.5","stream":false,"input":"hello"}`))
- if tt.wantError {
- require.Error(t, err)
- require.Nil(t, result)
- var failoverErr *UpstreamFailoverError
- require.False(t, errors.As(err, &failoverErr))
- } else {
- require.NoError(t, err)
- require.NotNil(t, result)
- }
- mark := GetOpsCyberPolicy(c)
- require.NotNil(t, mark)
- require.Equal(t, "cyber_policy", mark.Code)
- require.Equal(t, tt.wantInput, mark.UpstreamInTok)
- require.Equal(t, tt.wantOutput, mark.UpstreamOutTok)
- })
- }
- }
|