mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
Merge pull request #6254 from wucm667/fix/issue-6249-configurable-image-cooldown
feat(openai): configure image tool cooldown
This commit is contained in:
@@ -154,6 +154,33 @@ func (h *SettingHandler) UpdateRateLimit429CooldownSettings(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SettingHandler) GetOpenAIImagesOAuthUnavailableCooldownSettings(c *gin.Context) {
|
||||
settings, err := h.settingService.GetOpenAIImagesOAuthUnavailableCooldownSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, dto.OpenAIImagesOAuthUnavailableCooldownSettings{CooldownMinutes: settings.CooldownMinutes})
|
||||
}
|
||||
|
||||
type UpdateOpenAIImagesOAuthUnavailableCooldownSettingsRequest struct {
|
||||
CooldownMinutes int `json:"cooldown_minutes"`
|
||||
}
|
||||
|
||||
func (h *SettingHandler) UpdateOpenAIImagesOAuthUnavailableCooldownSettings(c *gin.Context) {
|
||||
var req UpdateOpenAIImagesOAuthUnavailableCooldownSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
settings := &service.OpenAIImagesOAuthUnavailableCooldownSettings{CooldownMinutes: req.CooldownMinutes}
|
||||
if err := h.settingService.SetOpenAIImagesOAuthUnavailableCooldownSettings(c.Request.Context(), settings); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.Success(c, dto.OpenAIImagesOAuthUnavailableCooldownSettings{CooldownMinutes: settings.CooldownMinutes})
|
||||
}
|
||||
|
||||
// GetPanelRateLimitSettings 获取面板 API 限流配置
|
||||
// GET /api/v1/admin/settings/panel-rate-limit
|
||||
func (h *SettingHandler) GetPanelRateLimitSettings(c *gin.Context) {
|
||||
|
||||
@@ -448,6 +448,10 @@ type RateLimit429CooldownSettings struct {
|
||||
CooldownSeconds int `json:"cooldown_seconds"`
|
||||
}
|
||||
|
||||
type OpenAIImagesOAuthUnavailableCooldownSettings struct {
|
||||
CooldownMinutes int `json:"cooldown_minutes"`
|
||||
}
|
||||
|
||||
// PanelRateLimitSettings 面板 API 限流配置 DTO
|
||||
type PanelRateLimitSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
@@ -574,6 +574,9 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
||||
// 429默认回避配置
|
||||
adminSettings.GET("/rate-limit-429-cooldown", h.Admin.Setting.GetRateLimit429CooldownSettings)
|
||||
adminSettings.PUT("/rate-limit-429-cooldown", h.Admin.Setting.UpdateRateLimit429CooldownSettings)
|
||||
// OpenAI OAuth image-tool unavailable cooldown configuration
|
||||
adminSettings.GET("/openai-images-oauth-unavailable-cooldown", h.Admin.Setting.GetOpenAIImagesOAuthUnavailableCooldownSettings)
|
||||
adminSettings.PUT("/openai-images-oauth-unavailable-cooldown", h.Admin.Setting.UpdateOpenAIImagesOAuthUnavailableCooldownSettings)
|
||||
// 面板 API 限流配置
|
||||
adminSettings.GET("/panel-rate-limit", h.Admin.Setting.GetPanelRateLimitSettings)
|
||||
adminSettings.PUT("/panel-rate-limit", h.Admin.Setting.UpdatePanelRateLimitSettings)
|
||||
|
||||
@@ -537,6 +537,8 @@ const (
|
||||
|
||||
// SettingKeyRateLimit429CooldownSettings stores JSON config for 429 fallback cooldown handling.
|
||||
SettingKeyRateLimit429CooldownSettings = "rate_limit_429_cooldown_settings"
|
||||
// SettingKeyOpenAIImagesOAuthUnavailableCooldownSettings stores the cooldown applied when the OAuth image tool is unavailable.
|
||||
SettingKeyOpenAIImagesOAuthUnavailableCooldownSettings = "openai_images_oauth_unavailable_cooldown_settings"
|
||||
// SettingKeyOpenAIAPIKeyHealthBreakerSettings stores the opt-in OpenAI pool API-key breaker config.
|
||||
SettingKeyOpenAIAPIKeyHealthBreakerSettings = "openai_apikey_health_breaker_settings"
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOpenAIImagesOAuthUnavailableCooldownSettingsDefaultAndStoredValue(t *testing.T) {
|
||||
repo := newMockSettingRepo()
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
settings, err := svc.GetOpenAIImagesOAuthUnavailableCooldownSettings(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 30, settings.CooldownMinutes)
|
||||
|
||||
require.NoError(t, svc.SetOpenAIImagesOAuthUnavailableCooldownSettings(context.Background(), &OpenAIImagesOAuthUnavailableCooldownSettings{CooldownMinutes: 7}))
|
||||
settings, err = svc.GetOpenAIImagesOAuthUnavailableCooldownSettings(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 7, settings.CooldownMinutes)
|
||||
}
|
||||
|
||||
func TestSetOpenAIImagesOAuthUnavailableCooldownSettingsBoundaries(t *testing.T) {
|
||||
svc := NewSettingService(newMockSettingRepo(), &config.Config{})
|
||||
|
||||
for _, minutes := range []int{1, openAIImagesOAuthUnavailableMaxCooldownMinutes} {
|
||||
err := svc.SetOpenAIImagesOAuthUnavailableCooldownSettings(context.Background(), &OpenAIImagesOAuthUnavailableCooldownSettings{CooldownMinutes: minutes})
|
||||
require.NoError(t, err, "should accept cooldown_minutes=%d", minutes)
|
||||
}
|
||||
|
||||
for _, minutes := range []int{0, -1, openAIImagesOAuthUnavailableMaxCooldownMinutes + 1} {
|
||||
err := svc.SetOpenAIImagesOAuthUnavailableCooldownSettings(context.Background(), &OpenAIImagesOAuthUnavailableCooldownSettings{CooldownMinutes: minutes})
|
||||
require.ErrorContains(t, err, "cooldown_minutes must be between 1-120")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIImagesOAuthUnavailableCooldownSettingsRejectsOverflowingValue(t *testing.T) {
|
||||
maxInt := int(^uint(0) >> 1)
|
||||
repo := newMockSettingRepo()
|
||||
svc := NewSettingService(repo, &config.Config{})
|
||||
|
||||
err := svc.SetOpenAIImagesOAuthUnavailableCooldownSettings(context.Background(), &OpenAIImagesOAuthUnavailableCooldownSettings{CooldownMinutes: maxInt})
|
||||
require.ErrorContains(t, err, "cooldown_minutes must be between 1-120")
|
||||
|
||||
for _, minutes := range []int{openAIImagesOAuthUnavailableMaxCooldownMinutes + 1, maxInt} {
|
||||
data, marshalErr := json.Marshal(OpenAIImagesOAuthUnavailableCooldownSettings{CooldownMinutes: minutes})
|
||||
require.NoError(t, marshalErr)
|
||||
repo.data[SettingKeyOpenAIImagesOAuthUnavailableCooldownSettings] = string(data)
|
||||
|
||||
settings, getErr := svc.GetOpenAIImagesOAuthUnavailableCooldownSettings(context.Background())
|
||||
require.NoError(t, getErr)
|
||||
require.Equal(t, openAIImagesOAuthUnavailableDefaultCooldownMinutes, settings.CooldownMinutes)
|
||||
}
|
||||
}
|
||||
@@ -1950,8 +1950,8 @@ func (s *OpenAIGatewayService) forwardOpenAIImagesOAuth(
|
||||
}
|
||||
|
||||
const (
|
||||
openAIImagesOAuthUnavailableCooldown = 30 * time.Minute
|
||||
openAIImagesOAuthUnavailableReason = "openai_images_oauth_tool_unavailable"
|
||||
openAIImagesOAuthUnavailableDefaultCooldown = 30 * time.Minute
|
||||
openAIImagesOAuthUnavailableReason = "openai_images_oauth_tool_unavailable"
|
||||
)
|
||||
|
||||
// shouldCoolOpenAIImagesToolForError decides whether an image_generation_unavailable
|
||||
@@ -1980,7 +1980,16 @@ func (s *OpenAIGatewayService) coolOpenAIImagesOAuthTool(ctx context.Context, ac
|
||||
}
|
||||
stateCtx, cancel := openAIAccountStateContext(ctx)
|
||||
defer cancel()
|
||||
resetAt := time.Now().Add(openAIImagesOAuthUnavailableCooldown)
|
||||
cooldown := openAIImagesOAuthUnavailableDefaultCooldown
|
||||
if s.settingService != nil {
|
||||
settings, err := s.settingService.GetOpenAIImagesOAuthUnavailableCooldownSettings(stateCtx)
|
||||
if err != nil {
|
||||
logger.LegacyPrintf("service.openai_gateway", "[OpenAI] Images OAuth tool cooldown setting read failed error=%v", err)
|
||||
} else {
|
||||
cooldown = time.Duration(settings.CooldownMinutes) * time.Minute
|
||||
}
|
||||
}
|
||||
resetAt := time.Now().Add(cooldown)
|
||||
if err := s.accountRepo.SetModelRateLimit(stateCtx, account.ID, openAIImageGenerationRateLimitKey, resetAt, openAIImagesOAuthUnavailableReason); err != nil {
|
||||
logger.LegacyPrintf("service.openai_gateway", "[OpenAI] Images OAuth tool cooldown write failed account_id=%d error=%v", account.ID, err)
|
||||
return
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -220,7 +221,23 @@ func TestOpenAIGatewayServiceForwardImages_StructuredUnavailableCoolsImageCapabi
|
||||
require.Equal(t, account.ID, call.accountID)
|
||||
require.Equal(t, openAIImageGenerationRateLimitKey, call.scope)
|
||||
require.Equal(t, openAIImagesOAuthUnavailableReason, call.reason)
|
||||
require.WithinDuration(t, before.Add(openAIImagesOAuthUnavailableCooldown), call.resetAt, time.Second)
|
||||
require.WithinDuration(t, before.Add(openAIImagesOAuthUnavailableDefaultCooldown), call.resetAt, time.Second)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayService_CoolOpenAIImagesOAuthToolUsesConfiguredCooldown(t *testing.T) {
|
||||
accountRepo := &modelNotFoundAccountRepoStub{}
|
||||
settingRepo := newMockSettingRepo()
|
||||
settingRepo.data[SettingKeyOpenAIImagesOAuthUnavailableCooldownSettings] = `{"cooldown_minutes":7}`
|
||||
svc := &OpenAIGatewayService{
|
||||
accountRepo: accountRepo,
|
||||
settingService: NewSettingService(settingRepo, &config.Config{}),
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
svc.coolOpenAIImagesOAuthTool(context.Background(), &Account{ID: 206, Platform: PlatformOpenAI, Type: AccountTypeOAuth})
|
||||
|
||||
require.Len(t, accountRepo.modelRateLimitCalls, 1)
|
||||
require.WithinDuration(t, before.Add(7*time.Minute), accountRepo.modelRateLimitCalls[0].resetAt, time.Second)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceForwardImages_CapabilityLossCoolsImageScope(t *testing.T) {
|
||||
|
||||
@@ -764,6 +764,40 @@ func (s *SettingService) SetRateLimit429CooldownSettings(ctx context.Context, se
|
||||
return s.settingRepo.Set(ctx, SettingKeyRateLimit429CooldownSettings, string(data))
|
||||
}
|
||||
|
||||
func (s *SettingService) GetOpenAIImagesOAuthUnavailableCooldownSettings(ctx context.Context) (*OpenAIImagesOAuthUnavailableCooldownSettings, error) {
|
||||
value, err := s.settingRepo.GetValue(ctx, SettingKeyOpenAIImagesOAuthUnavailableCooldownSettings)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrSettingNotFound) {
|
||||
return DefaultOpenAIImagesOAuthUnavailableCooldownSettings(), nil
|
||||
}
|
||||
return nil, fmt.Errorf("get OpenAI images OAuth unavailable cooldown settings: %w", err)
|
||||
}
|
||||
if value == "" {
|
||||
return DefaultOpenAIImagesOAuthUnavailableCooldownSettings(), nil
|
||||
}
|
||||
|
||||
var settings OpenAIImagesOAuthUnavailableCooldownSettings
|
||||
if err := json.Unmarshal([]byte(value), &settings); err != nil ||
|
||||
settings.CooldownMinutes <= 0 || settings.CooldownMinutes > openAIImagesOAuthUnavailableMaxCooldownMinutes {
|
||||
return DefaultOpenAIImagesOAuthUnavailableCooldownSettings(), nil
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetOpenAIImagesOAuthUnavailableCooldownSettings(ctx context.Context, settings *OpenAIImagesOAuthUnavailableCooldownSettings) error {
|
||||
if settings == nil {
|
||||
return fmt.Errorf("settings cannot be nil")
|
||||
}
|
||||
if settings.CooldownMinutes <= 0 || settings.CooldownMinutes > openAIImagesOAuthUnavailableMaxCooldownMinutes {
|
||||
return fmt.Errorf("cooldown_minutes must be between 1-%d", openAIImagesOAuthUnavailableMaxCooldownMinutes)
|
||||
}
|
||||
data, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal OpenAI images OAuth unavailable cooldown settings: %w", err)
|
||||
}
|
||||
return s.settingRepo.Set(ctx, SettingKeyOpenAIImagesOAuthUnavailableCooldownSettings, string(data))
|
||||
}
|
||||
|
||||
// GetStreamTimeoutSettings 获取流超时处理配置
|
||||
func (s *SettingService) GetStreamTimeoutSettings(ctx context.Context) (*StreamTimeoutSettings, error) {
|
||||
value, err := s.settingRepo.GetValue(ctx, SettingKeyStreamTimeoutSettings)
|
||||
|
||||
@@ -565,6 +565,16 @@ type RateLimit429CooldownSettings struct {
|
||||
CooldownSeconds int `json:"cooldown_seconds"`
|
||||
}
|
||||
|
||||
// OpenAIImagesOAuthUnavailableCooldownSettings controls how long an OAuth account's image capability is paused when unavailable.
|
||||
type OpenAIImagesOAuthUnavailableCooldownSettings struct {
|
||||
CooldownMinutes int `json:"cooldown_minutes"`
|
||||
}
|
||||
|
||||
const (
|
||||
openAIImagesOAuthUnavailableDefaultCooldownMinutes = 30
|
||||
openAIImagesOAuthUnavailableMaxCooldownMinutes = 120
|
||||
)
|
||||
|
||||
// OpenAIAPIKeyHealthBreakerSettings controls cross-instance failure counting for OpenAI pool API keys.
|
||||
type OpenAIAPIKeyHealthBreakerSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
@@ -598,6 +608,10 @@ func DefaultRateLimit429CooldownSettings() *RateLimit429CooldownSettings {
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultOpenAIImagesOAuthUnavailableCooldownSettings() *OpenAIImagesOAuthUnavailableCooldownSettings {
|
||||
return &OpenAIImagesOAuthUnavailableCooldownSettings{CooldownMinutes: openAIImagesOAuthUnavailableDefaultCooldownMinutes}
|
||||
}
|
||||
|
||||
// DefaultBetaPolicySettings 返回默认的 Beta 策略配置
|
||||
//
|
||||
// context-1m-2025-08-07 的默认策略:
|
||||
|
||||
Reference in New Issue
Block a user