Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/cmd/server/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.105
0.1.106
100 changes: 100 additions & 0 deletions backend/internal/handler/admin/setting_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,106 @@ func (h *SettingHandler) GetStreamTimeoutSettings(c *gin.Context) {
})
}

func toExtremePerformanceSettingsDTO(settings *service.ExtremePerformanceSettings) dto.ExtremePerformanceSettings {
if settings == nil {
settings = service.DefaultExtremePerformanceSettings()
}
return dto.ExtremePerformanceSettings{
Enabled: settings.Enabled,
Admin: dto.ExtremePerformanceAdminSettings{
DisableAutoUsageFetch: settings.Admin.DisableAutoUsageFetch,
DisableAutoTodayStatsFetch: settings.Admin.DisableAutoTodayStatsFetch,
AllowManualUsageFetch: settings.Admin.AllowManualUsageFetch,
},
Pool: dto.ExtremePerformancePoolSettings{
PlatformLimits: dto.ExtremePerformancePlatformLimits{
OpenAI: settings.Pool.PlatformLimits.OpenAI,
Gemini: settings.Pool.PlatformLimits.Gemini,
Anthropic: settings.Pool.PlatformLimits.Anthropic,
},
RefillTriggerGap: settings.Pool.RefillTriggerGap,
RefillBatchSize: settings.Pool.RefillBatchSize,
SelectionOrder: settings.Pool.SelectionOrder,
},
AccountPolicy: dto.ExtremePerformanceAccountPolicySettings{
DeleteOnAnyUpstream401: settings.AccountPolicy.DeleteOnAnyUpstream401,
CooldownOn429Minutes: settings.AccountPolicy.CooldownOn429Minutes,
CooldownOn5xxMinutes: settings.AccountPolicy.CooldownOn5xxMinutes,
RemoveFromHotPoolOnOverload: settings.AccountPolicy.RemoveFromHotPoolOnOverload,
RemoveFromHotPoolOnTempUnschedulable: settings.AccountPolicy.RemoveFromHotPoolOnTempUnschedulable,
},
}
}

func toExtremePerformanceSettingsModel(settings dto.ExtremePerformanceSettings) *service.ExtremePerformanceSettings {
return &service.ExtremePerformanceSettings{
Enabled: settings.Enabled,
Admin: service.ExtremePerformanceAdminSettings{
DisableAutoUsageFetch: settings.Admin.DisableAutoUsageFetch,
DisableAutoTodayStatsFetch: settings.Admin.DisableAutoTodayStatsFetch,
AllowManualUsageFetch: settings.Admin.AllowManualUsageFetch,
},
Pool: service.ExtremePerformancePoolSettings{
PlatformLimits: service.ExtremePerformancePlatformLimits{
OpenAI: settings.Pool.PlatformLimits.OpenAI,
Gemini: settings.Pool.PlatformLimits.Gemini,
Anthropic: settings.Pool.PlatformLimits.Anthropic,
},
RefillTriggerGap: settings.Pool.RefillTriggerGap,
RefillBatchSize: settings.Pool.RefillBatchSize,
SelectionOrder: strings.TrimSpace(settings.Pool.SelectionOrder),
},
AccountPolicy: service.ExtremePerformanceAccountPolicySettings{
DeleteOnAnyUpstream401: settings.AccountPolicy.DeleteOnAnyUpstream401,
CooldownOn429Minutes: settings.AccountPolicy.CooldownOn429Minutes,
CooldownOn5xxMinutes: settings.AccountPolicy.CooldownOn5xxMinutes,
RemoveFromHotPoolOnOverload: settings.AccountPolicy.RemoveFromHotPoolOnOverload,
RemoveFromHotPoolOnTempUnschedulable: settings.AccountPolicy.RemoveFromHotPoolOnTempUnschedulable,
},
}
}

// GetExtremePerformanceSettings 获取极致性能模式配置
// GET /api/v1/admin/settings/extreme-performance
func (h *SettingHandler) GetExtremePerformanceSettings(c *gin.Context) {
settings, err := h.settingService.GetExtremePerformanceSettings(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}

response.Success(c, toExtremePerformanceSettingsDTO(settings))
}

// UpdateExtremePerformanceSettings 更新极致性能模式配置
// PUT /api/v1/admin/settings/extreme-performance
func (h *SettingHandler) UpdateExtremePerformanceSettings(c *gin.Context) {
var req dto.ExtremePerformanceSettings
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}

settings := toExtremePerformanceSettingsModel(req)
if err := service.ValidateExtremePerformanceSettings(settings); err != nil {
response.BadRequest(c, err.Error())
return
}

if err := h.settingService.SetExtremePerformanceSettings(c.Request.Context(), settings); err != nil {
response.BadRequest(c, err.Error())
return
}

updatedSettings, err := h.settingService.GetExtremePerformanceSettings(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}

response.Success(c, toExtremePerformanceSettingsDTO(updatedSettings))
}

func toSoraS3SettingsDTO(settings *service.SoraS3Settings) dto.SoraS3Settings {
if settings == nil {
return dto.SoraS3Settings{}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package admin

import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)

type extremePerformanceHandlerRepoStub struct {
values map[string]string
}

func (s *extremePerformanceHandlerRepoStub) Get(ctx context.Context, key string) (*service.Setting, error) {
panic("unexpected Get call")
}

func (s *extremePerformanceHandlerRepoStub) GetValue(ctx context.Context, key string) (string, error) {
if s.values == nil {
return "", service.ErrSettingNotFound
}
value, ok := s.values[key]
if !ok {
return "", service.ErrSettingNotFound
}
return value, nil
}

func (s *extremePerformanceHandlerRepoStub) Set(ctx context.Context, key, value string) error {
if s.values == nil {
s.values = make(map[string]string)
}
s.values[key] = value
return nil
}

func (s *extremePerformanceHandlerRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
panic("unexpected GetMultiple call")
}

func (s *extremePerformanceHandlerRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
panic("unexpected SetMultiple call")
}

func (s *extremePerformanceHandlerRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
panic("unexpected GetAll call")
}

func (s *extremePerformanceHandlerRepoStub) Delete(ctx context.Context, key string) error {
panic("unexpected Delete call")
}

type extremePerformanceResponseEnvelope struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}

func newExtremePerformanceTestHandler(t *testing.T) (*SettingHandler, *extremePerformanceHandlerRepoStub) {
t.Helper()
repo := &extremePerformanceHandlerRepoStub{}
svc := service.NewSettingService(repo, &config.Config{})
return NewSettingHandler(svc, nil, nil, nil, nil), repo
}

func TestSettingHandler_GetExtremePerformanceSettings(t *testing.T) {
gin.SetMode(gin.TestMode)
h, _ := newExtremePerformanceTestHandler(t)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/settings/extreme-performance", nil)

h.GetExtremePerformanceSettings(c)

require.Equal(t, http.StatusOK, w.Code)
var envelope extremePerformanceResponseEnvelope
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &envelope))
require.Equal(t, 0, envelope.Code)

var data map[string]any
require.NoError(t, json.Unmarshal(envelope.Data, &data))
require.Equal(t, false, data["enabled"])
admin, ok := data["admin"].(map[string]any)
require.True(t, ok)
require.Equal(t, true, admin["disable_auto_usage_fetch"])
}

func TestSettingHandler_UpdateExtremePerformanceSettings(t *testing.T) {
gin.SetMode(gin.TestMode)
h, repo := newExtremePerformanceTestHandler(t)
body := []byte(`{
"enabled": true,
"admin": {
"disable_auto_usage_fetch": true,
"disable_auto_today_stats_fetch": true,
"allow_manual_usage_fetch": false
},
"pool": {
"platform_limits": {
"openai": 1200,
"gemini": 240,
"anthropic": 80
},
"refill_trigger_gap": 25,
"refill_batch_size": 40,
"selection_order": "imported_first"
},
"account_policy": {
"delete_on_any_upstream_401": true,
"cooldown_on_429_minutes": 5,
"cooldown_on_5xx_minutes": 1,
"remove_from_hot_pool_on_overload": true,
"remove_from_hot_pool_on_temp_unschedulable": true
}
}`)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings/extreme-performance", bytes.NewReader(body))
c.Request.Header.Set("Content-Type", "application/json")

h.UpdateExtremePerformanceSettings(c)

require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, repo.values, service.SettingKeyExtremePerformanceSettings)

var envelope extremePerformanceResponseEnvelope
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &envelope))
require.Equal(t, 0, envelope.Code)

var data map[string]any
require.NoError(t, json.Unmarshal(envelope.Data, &data))
require.Equal(t, true, data["enabled"])
}

func TestSettingHandler_UpdateExtremePerformanceSettings_RejectsInvalidSelectionOrder(t *testing.T) {
gin.SetMode(gin.TestMode)
h, _ := newExtremePerformanceTestHandler(t)
body := []byte(`{
"enabled": true,
"admin": {
"disable_auto_usage_fetch": true,
"disable_auto_today_stats_fetch": true,
"allow_manual_usage_fetch": true
},
"pool": {
"platform_limits": {
"openai": 1200,
"gemini": 240,
"anthropic": 80
},
"refill_trigger_gap": 25,
"refill_batch_size": 40,
"selection_order": "random"
},
"account_policy": {
"delete_on_any_upstream_401": true,
"cooldown_on_429_minutes": 5,
"cooldown_on_5xx_minutes": 1,
"remove_from_hot_pool_on_overload": true,
"remove_from_hot_pool_on_temp_unschedulable": true
}
}`)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings/extreme-performance", bytes.NewReader(body))
c.Request.Header.Set("Content-Type", "application/json")

h.UpdateExtremePerformanceSettings(c)

require.Equal(t, http.StatusBadRequest, w.Code)
var envelope response.Response
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &envelope))
require.Contains(t, envelope.Message, "selection_order")
}
56 changes: 56 additions & 0 deletions backend/internal/handler/dto/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,63 @@ type StreamTimeoutSettings struct {
ThresholdWindowMinutes int `json:"threshold_window_minutes"`
}

// ExtremePerformanceAdminSettings 极致性能模式后台轻量化配置 DTO
type ExtremePerformanceAdminSettings struct {
DisableAutoUsageFetch bool `json:"disable_auto_usage_fetch"`
DisableAutoTodayStatsFetch bool `json:"disable_auto_today_stats_fetch"`
AllowManualUsageFetch bool `json:"allow_manual_usage_fetch"`
}

// ExtremePerformancePlatformLimits 极致性能模式热池平台上限 DTO
type ExtremePerformancePlatformLimits struct {
OpenAI int `json:"openai"`
Gemini int `json:"gemini"`
Anthropic int `json:"anthropic"`
}

// ExtremePerformancePoolSettings 极致性能模式热池配置 DTO
type ExtremePerformancePoolSettings struct {
PlatformLimits ExtremePerformancePlatformLimits `json:"platform_limits"`
RefillTriggerGap int `json:"refill_trigger_gap"`
RefillBatchSize int `json:"refill_batch_size"`
SelectionOrder string `json:"selection_order"`
}

// ExtremePerformanceAccountPolicySettings 极致性能模式账号策略配置 DTO
type ExtremePerformanceAccountPolicySettings struct {
DeleteOnAnyUpstream401 bool `json:"delete_on_any_upstream_401"`
CooldownOn429Minutes int `json:"cooldown_on_429_minutes"`
CooldownOn5xxMinutes int `json:"cooldown_on_5xx_minutes"`
RemoveFromHotPoolOnOverload bool `json:"remove_from_hot_pool_on_overload"`
RemoveFromHotPoolOnTempUnschedulable bool `json:"remove_from_hot_pool_on_temp_unschedulable"`
}

// ExtremePerformanceSettings 极致性能模式配置 DTO
type ExtremePerformanceSettings struct {
Enabled bool `json:"enabled"`
Admin ExtremePerformanceAdminSettings `json:"admin"`
Pool ExtremePerformancePoolSettings `json:"pool"`
AccountPolicy ExtremePerformanceAccountPolicySettings `json:"account_policy"`
}

// ExtremePerformancePlatformStatus 极致性能模式平台运行态 DTO
type ExtremePerformancePlatformStatus struct {
Platform string `json:"platform"`
TargetSize int `json:"target_size"`
CurrentSize int `json:"current_size"`
ColdCandidates int `json:"cold_candidates,omitempty"`
LastRefillAt string `json:"last_refill_at,omitempty"`
LastRebuildAt string `json:"last_rebuild_at,omitempty"`
Version int64 `json:"version"`
}

// ExtremePerformanceStatus 极致性能模式运行态 DTO
type ExtremePerformanceStatus struct {
Platforms []ExtremePerformancePlatformStatus `json:"platforms"`
}

// RectifierSettings 请求整流器配置 DTO

type RectifierSettings struct {
Enabled bool `json:"enabled"`
ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"`
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/server/routes/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,9 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
// 流超时处理配置
adminSettings.GET("/stream-timeout", h.Admin.Setting.GetStreamTimeoutSettings)
adminSettings.PUT("/stream-timeout", h.Admin.Setting.UpdateStreamTimeoutSettings)
// 极致性能模式配置
adminSettings.GET("/extreme-performance", h.Admin.Setting.GetExtremePerformanceSettings)
adminSettings.PUT("/extreme-performance", h.Admin.Setting.UpdateExtremePerformanceSettings)
// 请求整流器配置
adminSettings.GET("/rectifier", h.Admin.Setting.GetRectifierSettings)
adminSettings.PUT("/rectifier", h.Admin.Setting.UpdateRectifierSettings)
Expand Down
7 changes: 7 additions & 0 deletions backend/internal/service/domain_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ const (
// SettingKeyStreamTimeoutSettings stores JSON config for stream timeout handling.
SettingKeyStreamTimeoutSettings = "stream_timeout_settings"

// =========================
// Extreme Performance Mode
// =========================

// SettingKeyExtremePerformanceSettings stores JSON config for extreme performance mode.
SettingKeyExtremePerformanceSettings = "extreme_performance_settings"

// =========================
// Request Rectifier (请求整流器)
// =========================
Expand Down
Loading