feat: optionally enforce API-key spending windows in simple mode

This commit is contained in:
zhangcj committed 2026-09-23 08:17:48 +08:00
1 parent 20a94fbb56
commit 3b336b2eca
18 files changed
+509 -36

No files matched your search

@@ -1916,7 +1916,9 @@ func newOpenAIWSHandlerTestServer(t *testing.T, h *OpenAIGatewayHandler, subject
}
type openAIResponsesWSUsageLogCase struct {
firstPayload string
simpleModeRejectAtRead int64
closeReason string
firstPayload string
// midPayload 在首个 turn 完成后发送(如 session.update),上游桩会为它
// 回一个 response.completed,客户端按普通事件读取。
midPayload string
@@ -2963,7 +2965,13 @@ func runOpenAIResponsesWebSocketUsageLogCase(t *testing.T, tc openAIResponsesWSU
}, nil, nil, nil, nil)
}
billingCacheSvc := service.NewBillingCacheService(nil, nil, nil, nil, nil, nil, cfg, nil)
var keyRepo service.APIKeyRepository
if tc.simpleModeRejectAtRead > 0 {
cfg.SimpleModeKeyRateLimitEnabled = true
keyRepo = &simpleModeWSRateLimitRepo{rejectAt: tc.simpleModeRejectAtRead}
}
billingCacheSvc := service.NewBillingCacheService(nil, nil, nil, keyRepo, nil, nil, cfg, nil)
t.Cleanup(billingCacheSvc.Stop)
gatewaySvc := service.NewOpenAIGatewayService(
accountRepo,
usageRepo,
@@ -2998,6 +3006,7 @@ func runOpenAIResponsesWebSocketUsageLogCase(t *testing.T, tc openAIResponsesWSU
},
}
h := &OpenAIGatewayHandler{
cfg: cfg,
gatewayService: gatewaySvc,
billingCacheService: billingCacheSvc,
apiKeyService: &service.APIKeyService{},
@@ -3009,6 +3018,9 @@ func runOpenAIResponsesWebSocketUsageLogCase(t *testing.T, tc openAIResponsesWSU
GroupID: &groupID,
User: &service.User{ID: 1701, Status: service.StatusActive},
}
if tc.simpleModeRejectAtRead > 0 {
apiKey.RateLimit5h = 1
}
if tc.group != nil {
apiKey.Group = tc.group
}
@@ -3051,7 +3063,11 @@ func runOpenAIResponsesWebSocketUsageLogCase(t *testing.T, tc openAIResponsesWSU
var closeErr coderws.CloseError
require.ErrorAs(t, readErr, &closeErr)
require.Equal(t, coderws.StatusPolicyViolation, closeErr.Code)
require.Contains(t, closeErr.Reason, "not available for this group")
reason := tc.closeReason
if reason == "" {
reason = "not available for this group"
}
require.Contains(t, closeErr.Reason, reason)
_ = clientConn.CloseNow()
return openAIResponsesWSUsageLogResult{}
}
@@ -3086,7 +3102,11 @@ func runOpenAIResponsesWebSocketUsageLogCase(t *testing.T, tc openAIResponsesWSU
var closeErr coderws.CloseError
require.ErrorAs(t, readErr, &closeErr)
require.Equal(t, coderws.StatusPolicyViolation, closeErr.Code)
require.Contains(t, closeErr.Reason, "not available for this group")
reason := tc.closeReason
if reason == "" {
reason = "not available for this group"
}
require.Contains(t, closeErr.Reason, reason)
_ = clientConn.CloseNow()
return openAIResponsesWSUsageLogResult{}
}
@@ -3199,3 +3219,37 @@ data: {"type":"response.failed","error":{"message":"This content was flagged"}}
require.False(t, openAIForwardErrorAlreadyCommunicated(c, c.Writer.Size(), errors.New("openai cyber_policy: blocked")))
})
}
// The loader simulates window usage reaching its limit while a connection is
// waiting for its first account or between completed WebSocket turns.
type simpleModeWSRateLimitRepo struct {
service.APIKeyRepository
reads atomic.Int64
rejectAt int64
}
func (r *simpleModeWSRateLimitRepo) GetRateLimitData(context.Context, int64) (*service.APIKeyRateLimitData, error) {
now := time.Now()
usage := 0.0
if r.reads.Add(1) >= r.rejectAt {
usage = 1
}
return &service.APIKeyRateLimitData{Usage5h: usage, Window5hStart: &now}, nil
}
func TestOpenAIResponsesWebSocketSimpleModeRechecksKeyWindows(t *testing.T) {
for _, mode := range []string{service.OpenAIWSIngressModePassthrough, service.OpenAIWSIngressModeCtxPool} {
t.Run(mode+"/after-account-selection", func(t *testing.T) {
runOpenAIResponsesWebSocketUsageLogCase(t, openAIResponsesWSUsageLogCase{
firstPayload: `{"type":"response.create","model":"gpt-5.1"}`,
ingressMode: mode, simpleModeRejectAtRead: 2, firstFrameCloseExpected: true, closeReason: "billing check failed",
})
})
t.Run(mode+"/followup-turn", func(t *testing.T) {
runOpenAIResponsesWebSocketUsageLogCase(t, openAIResponsesWSUsageLogCase{
firstPayload: `{"type":"response.create","model":"gpt-5.1"}`,
secondPayload: `{"type":"response.create","model":"gpt-5.1"}`,
ingressMode: mode, simpleModeRejectAtRead: 3, secondTurnCloseExpected: true, closeReason: "billing check failed",
})
})
}
}