fix(scheduler): 为调度倍率补充账号倍率回退

This commit is contained in:
Brisbanehuang
2026-09-13 00:14:16 -04:00
parent bdb42e22f8
commit b18e4ce455
16 changed files with 387 additions and 94 deletions
@@ -506,7 +506,7 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.OpenAILowUpstreamRatePriorityEnabled != after.OpenAILowUpstreamRatePriorityEnabled {
changed = append(changed, "openai_low_upstream_rate_priority_enabled")
}
if before.OpenAIOAuthSchedulingRateMultiplier != after.OpenAIOAuthSchedulingRateMultiplier {
if !equalNullableFloat(before.OpenAIOAuthSchedulingRateMultiplier, after.OpenAIOAuthSchedulingRateMultiplier) {
changed = append(changed, "openai_oauth_scheduling_rate_multiplier")
}
if before.OpenAIAdvancedSchedulerEnabled != after.OpenAIAdvancedSchedulerEnabled {
@@ -0,0 +1,76 @@
package admin
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func TestSettingHandler_OAuthSchedulingRateRoundTrip(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, tt := range []struct {
name string
initial map[string]string
body string
stored string
value any
status int
}{
{"absent keeps default", nil, `{}`, "1", float64(1), http.StatusOK},
{"omitted preserves override", map[string]string{service.SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.7"}, `{}`, "0.7", 0.7, http.StatusOK},
{"clear override", map[string]string{service.SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.7"}, `{"openai_oauth_scheduling_rate_multiplier":null}`, "", nil, http.StatusOK},
{"omitted preserves cleared", map[string]string{service.SettingKeyOpenAIOAuthSchedulingRateMultiplier: ""}, `{}`, "", nil, http.StatusOK},
{"set override", nil, `{"openai_oauth_scheduling_rate_multiplier":0.7}`, "0.7", 0.7, http.StatusOK},
{"zero is explicit", nil, `{"openai_oauth_scheduling_rate_multiplier":0}`, "0", float64(0), http.StatusOK},
{"negative is rejected", nil, `{"openai_oauth_scheduling_rate_multiplier":-1}`, "", nil, http.StatusBadRequest},
{"string is rejected", nil, `{"openai_oauth_scheduling_rate_multiplier":"invalid"}`, "", nil, http.StatusBadRequest},
} {
t.Run(tt.name, func(t *testing.T) {
repo := &settingHandlerRepoStub{values: tt.initial}
svc := service.NewSettingService(repo, &config.Config{})
handler := NewSettingHandler(svc, nil, nil, nil, nil, nil, nil)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings", bytes.NewBufferString(tt.body))
c.Request.Header.Set("Content-Type", "application/json")
handler.UpdateSettings(c)
require.Equal(t, tt.status, rec.Code, rec.Body.String())
if tt.status != http.StatusOK {
require.Empty(t, repo.lastUpdates)
return
}
require.Equal(t, tt.stored, repo.values[service.SettingKeyOpenAIOAuthSchedulingRateMultiplier])
var body struct {
Data map[string]any `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
require.Contains(t, body.Data, service.SettingKeyOpenAIOAuthSchedulingRateMultiplier)
require.Equal(t, tt.value, body.Data[service.SettingKeyOpenAIOAuthSchedulingRateMultiplier])
getRec := httptest.NewRecorder()
getContext, _ := gin.CreateTestContext(getRec)
getContext.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/settings", nil)
handler.GetSettings(getContext)
require.Equal(t, http.StatusOK, getRec.Code, getRec.Body.String())
require.NoError(t, json.Unmarshal(getRec.Body.Bytes(), &body))
require.Contains(t, body.Data, service.SettingKeyOpenAIOAuthSchedulingRateMultiplier)
require.Equal(t, tt.value, body.Data[service.SettingKeyOpenAIOAuthSchedulingRateMultiplier])
})
}
}
func TestSettingsAuditTracksOAuthRateValueNotPointer(t *testing.T) {
beforeRate, afterRate := 0.7, 0.7
before := &service.SystemSettings{OpenAIOAuthSchedulingRateMultiplier: &beforeRate}
after := &service.SystemSettings{OpenAIOAuthSchedulingRateMultiplier: &afterRate}
require.NotContains(t, diffSettings(before, after, nil, nil, UpdateSettingsRequest{}), service.SettingKeyOpenAIOAuthSchedulingRateMultiplier)
after.OpenAIOAuthSchedulingRateMultiplier = nil
require.Contains(t, diffSettings(before, after, nil, nil, UpdateSettingsRequest{}), service.SettingKeyOpenAIOAuthSchedulingRateMultiplier)
}
@@ -1809,9 +1809,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return previousSettings.OpenAILowUpstreamRatePriorityEnabled
}(),
OpenAIOAuthSchedulingRateMultiplier: func() float64 {
if req.OpenAIOAuthSchedulingRateMultiplier != nil {
return *req.OpenAIOAuthSchedulingRateMultiplier
OpenAIOAuthSchedulingRateMultiplier: func() *float64 {
// Omitted fields preserve the override; explicit null clears it.
if _, sent := sentFields[service.SettingKeyOpenAIOAuthSchedulingRateMultiplier]; sent {
return req.OpenAIOAuthSchedulingRateMultiplier
}
return previousSettings.OpenAIOAuthSchedulingRateMultiplier
}(),
+27 -27
View File
@@ -236,33 +236,33 @@ type SystemSettings struct {
PaymentVisibleMethodWxpayEnabled bool `json:"payment_visible_method_wxpay_enabled"`
// OpenAI account scheduling
OpenAILowUpstreamRatePriorityEnabled bool `json:"openai_low_upstream_rate_priority_enabled"`
OpenAIOAuthSchedulingRateMultiplier float64 `json:"openai_oauth_scheduling_rate_multiplier"`
OpenAIAdvancedSchedulerEnabled bool `json:"openai_advanced_scheduler_enabled"`
OpenAIAdvancedSchedulerStickyWeightedEnabled bool `json:"openai_advanced_scheduler_sticky_weighted_enabled"`
OpenAIAdvancedSchedulerSubscriptionPriorityEnabled bool `json:"openai_advanced_scheduler_subscription_priority_enabled"`
OpenAIAdvancedSchedulerLBTopK string `json:"openai_advanced_scheduler_lb_top_k"`
OpenAIAdvancedSchedulerWeightPriority string `json:"openai_advanced_scheduler_weight_priority"`
OpenAIAdvancedSchedulerWeightLoad string `json:"openai_advanced_scheduler_weight_load"`
OpenAIAdvancedSchedulerWeightQueue string `json:"openai_advanced_scheduler_weight_queue"`
OpenAIAdvancedSchedulerWeightErrorRate string `json:"openai_advanced_scheduler_weight_error_rate"`
OpenAIAdvancedSchedulerWeightTTFT string `json:"openai_advanced_scheduler_weight_ttft"`
OpenAIAdvancedSchedulerWeightReset string `json:"openai_advanced_scheduler_weight_reset"`
OpenAIAdvancedSchedulerWeightQuotaHeadroom string `json:"openai_advanced_scheduler_weight_quota_headroom"`
OpenAIAdvancedSchedulerWeightUpstreamCost string `json:"openai_advanced_scheduler_weight_upstream_cost"`
OpenAIAdvancedSchedulerWeightPreviousResponse string `json:"openai_advanced_scheduler_weight_previous_response"`
OpenAIAdvancedSchedulerWeightSessionSticky string `json:"openai_advanced_scheduler_weight_session_sticky"`
OpenAIAdvancedSchedulerEffectiveLBTopK string `json:"openai_advanced_scheduler_effective_lb_top_k"`
OpenAIAdvancedSchedulerEffectiveWeightPriority string `json:"openai_advanced_scheduler_effective_weight_priority"`
OpenAIAdvancedSchedulerEffectiveWeightLoad string `json:"openai_advanced_scheduler_effective_weight_load"`
OpenAIAdvancedSchedulerEffectiveWeightQueue string `json:"openai_advanced_scheduler_effective_weight_queue"`
OpenAIAdvancedSchedulerEffectiveWeightErrorRate string `json:"openai_advanced_scheduler_effective_weight_error_rate"`
OpenAIAdvancedSchedulerEffectiveWeightTTFT string `json:"openai_advanced_scheduler_effective_weight_ttft"`
OpenAIAdvancedSchedulerEffectiveWeightReset string `json:"openai_advanced_scheduler_effective_weight_reset"`
OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom string `json:"openai_advanced_scheduler_effective_weight_quota_headroom"`
OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost string `json:"openai_advanced_scheduler_effective_weight_upstream_cost"`
OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse string `json:"openai_advanced_scheduler_effective_weight_previous_response"`
OpenAIAdvancedSchedulerEffectiveWeightSessionSticky string `json:"openai_advanced_scheduler_effective_weight_session_sticky"`
OpenAILowUpstreamRatePriorityEnabled bool `json:"openai_low_upstream_rate_priority_enabled"`
OpenAIOAuthSchedulingRateMultiplier *float64 `json:"openai_oauth_scheduling_rate_multiplier"`
OpenAIAdvancedSchedulerEnabled bool `json:"openai_advanced_scheduler_enabled"`
OpenAIAdvancedSchedulerStickyWeightedEnabled bool `json:"openai_advanced_scheduler_sticky_weighted_enabled"`
OpenAIAdvancedSchedulerSubscriptionPriorityEnabled bool `json:"openai_advanced_scheduler_subscription_priority_enabled"`
OpenAIAdvancedSchedulerLBTopK string `json:"openai_advanced_scheduler_lb_top_k"`
OpenAIAdvancedSchedulerWeightPriority string `json:"openai_advanced_scheduler_weight_priority"`
OpenAIAdvancedSchedulerWeightLoad string `json:"openai_advanced_scheduler_weight_load"`
OpenAIAdvancedSchedulerWeightQueue string `json:"openai_advanced_scheduler_weight_queue"`
OpenAIAdvancedSchedulerWeightErrorRate string `json:"openai_advanced_scheduler_weight_error_rate"`
OpenAIAdvancedSchedulerWeightTTFT string `json:"openai_advanced_scheduler_weight_ttft"`
OpenAIAdvancedSchedulerWeightReset string `json:"openai_advanced_scheduler_weight_reset"`
OpenAIAdvancedSchedulerWeightQuotaHeadroom string `json:"openai_advanced_scheduler_weight_quota_headroom"`
OpenAIAdvancedSchedulerWeightUpstreamCost string `json:"openai_advanced_scheduler_weight_upstream_cost"`
OpenAIAdvancedSchedulerWeightPreviousResponse string `json:"openai_advanced_scheduler_weight_previous_response"`
OpenAIAdvancedSchedulerWeightSessionSticky string `json:"openai_advanced_scheduler_weight_session_sticky"`
OpenAIAdvancedSchedulerEffectiveLBTopK string `json:"openai_advanced_scheduler_effective_lb_top_k"`
OpenAIAdvancedSchedulerEffectiveWeightPriority string `json:"openai_advanced_scheduler_effective_weight_priority"`
OpenAIAdvancedSchedulerEffectiveWeightLoad string `json:"openai_advanced_scheduler_effective_weight_load"`
OpenAIAdvancedSchedulerEffectiveWeightQueue string `json:"openai_advanced_scheduler_effective_weight_queue"`
OpenAIAdvancedSchedulerEffectiveWeightErrorRate string `json:"openai_advanced_scheduler_effective_weight_error_rate"`
OpenAIAdvancedSchedulerEffectiveWeightTTFT string `json:"openai_advanced_scheduler_effective_weight_ttft"`
OpenAIAdvancedSchedulerEffectiveWeightReset string `json:"openai_advanced_scheduler_effective_weight_reset"`
OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom string `json:"openai_advanced_scheduler_effective_weight_quota_headroom"`
OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost string `json:"openai_advanced_scheduler_effective_weight_upstream_cost"`
OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse string `json:"openai_advanced_scheduler_effective_weight_previous_response"`
OpenAIAdvancedSchedulerEffectiveWeightSessionSticky string `json:"openai_advanced_scheduler_effective_weight_session_sticky"`
// Payment configuration
PaymentEnabled bool `json:"payment_enabled"`
@@ -45,7 +45,7 @@ const (
type cachedOpenAIAdvancedSchedulerSetting struct {
lowUpstreamRatePriorityEnabled bool
oauthSchedulingRateMultiplier float64
oauthSchedulingRateMultiplier *float64
enabled bool
stickyWeightedEnabled bool
subscriptionPriorityEnabled bool
@@ -56,7 +56,7 @@ type cachedOpenAIAdvancedSchedulerSetting struct {
type openAIAdvancedSchedulerRuntimeSettings struct {
lowUpstreamRatePriorityEnabled bool
oauthSchedulingRateMultiplier float64
oauthSchedulingRateMultiplier *float64
enabled bool
stickyWeightedEnabled bool
subscriptionPriorityEnabled bool
@@ -1904,7 +1904,7 @@ func (s *OpenAIGatewayService) openAIAdvancedSchedulerRuntimeSettings(ctx contex
}
lowUpstreamRatePriorityEnabled := false
oauthSchedulingRateMultiplier := defaultOpenAIOAuthSchedulingRateMultiplier
oauthSchedulingRateMultiplier := parseOpenAIOAuthSchedulingRateMultiplier(nil)
enabled := false
stickyWeightedEnabled := false
subscriptionPriorityEnabled := false
@@ -1916,7 +1916,7 @@ func (s *OpenAIGatewayService) openAIAdvancedSchedulerRuntimeSettings(ctx contex
if values, err := repo.GetMultiple(dbCtx, openAIAdvancedSchedulerRuntimeSettingKeys()); err == nil {
lowUpstreamRatePriorityEnabled = strings.EqualFold(strings.TrimSpace(values[SettingKeyOpenAILowUpstreamRatePriorityEnabled]), "true")
oauthSchedulingRateMultiplier = parseOpenAIOAuthSchedulingRateMultiplier(values[SettingKeyOpenAIOAuthSchedulingRateMultiplier])
oauthSchedulingRateMultiplier = parseOpenAIOAuthSchedulingRateMultiplier(values)
enabled = strings.EqualFold(strings.TrimSpace(values[openAIAdvancedSchedulerSettingKey]), "true")
stickyWeightedEnabled = strings.EqualFold(strings.TrimSpace(values[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled]), "true")
subscriptionPriorityEnabled = strings.EqualFold(strings.TrimSpace(values[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled]), "true")
@@ -1933,7 +1933,7 @@ func (s *OpenAIGatewayService) openAIAdvancedSchedulerRuntimeSettings(ctx contex
}
}
lowUpstreamRatePriorityEnabled = strings.EqualFold(strings.TrimSpace(fallbackValues[SettingKeyOpenAILowUpstreamRatePriorityEnabled]), "true")
oauthSchedulingRateMultiplier = parseOpenAIOAuthSchedulingRateMultiplier(fallbackValues[SettingKeyOpenAIOAuthSchedulingRateMultiplier])
oauthSchedulingRateMultiplier = parseOpenAIOAuthSchedulingRateMultiplier(fallbackValues)
enabled = strings.EqualFold(strings.TrimSpace(fallbackValues[openAIAdvancedSchedulerSettingKey]), "true")
stickyWeightedEnabled = strings.EqualFold(strings.TrimSpace(fallbackValues[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled]), "true")
subscriptionPriorityEnabled = strings.EqualFold(strings.TrimSpace(fallbackValues[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled]), "true")
@@ -1976,7 +1976,7 @@ func (s *OpenAIGatewayService) isOpenAILowUpstreamRatePriorityEnabled(ctx contex
return !settings.enabled && settings.lowUpstreamRatePriorityEnabled
}
func (s *OpenAIGatewayService) openAIOAuthSchedulingRateMultiplier(ctx context.Context) float64 {
func (s *OpenAIGatewayService) openAIOAuthSchedulingRateMultiplier(ctx context.Context) *float64 {
return s.openAIAdvancedSchedulerRuntimeSettings(ctx).oauthSchedulingRateMultiplier
}
@@ -2664,7 +2664,7 @@ func BuildOpenAIAccountSchedulerScoreSnapshot(
loadMap map[int64]*AccountLoadInfo,
) map[int64]OpenAIAccountSchedulerScoreSnapshot {
gateway := &OpenAIGatewayService{}
return buildOpenAIAccountSchedulerScoreSnapshot(accounts, loadMap, gateway.openAIWSSchedulerWeights(), false, defaultOpenAIOAuthSchedulingRateMultiplier)
return buildOpenAIAccountSchedulerScoreSnapshot(accounts, loadMap, gateway.openAIWSSchedulerWeights(), false, parseOpenAIOAuthSchedulingRateMultiplier(nil))
}
func buildOpenAIAccountSchedulerScoreSnapshot(
@@ -2672,7 +2672,7 @@ func buildOpenAIAccountSchedulerScoreSnapshot(
loadMap map[int64]*AccountLoadInfo,
weights GatewayOpenAIWSSchedulerScoreWeightsView,
stickyWeightedEnabled bool,
oauthSchedulingRateMultiplier float64,
oauthSchedulingRateMultiplier *float64,
) map[int64]OpenAIAccountSchedulerScoreSnapshot {
if len(accounts) == 0 {
return nil
@@ -2795,7 +2795,7 @@ func buildOpenAIAccountSchedulerScoreSnapshot(
return result
}
func openAIUpstreamCostFactors(accounts []*Account, now time.Time, oauthSchedulingRateMultiplier float64) map[int64]float64 {
func openAIUpstreamCostFactors(accounts []*Account, now time.Time, oauthSchedulingRateMultiplier *float64) map[int64]float64 {
type rateSample struct {
accountID int64
rate float64
@@ -2862,7 +2862,7 @@ type openAILegacyUpstreamRateOrder struct {
rates map[int64]float64
}
func newOpenAILegacyUpstreamRateOrder(accounts []*Account, now time.Time, oauthSchedulingRateMultiplier float64) openAILegacyUpstreamRateOrder {
func newOpenAILegacyUpstreamRateOrder(accounts []*Account, now time.Time, oauthSchedulingRateMultiplier *float64) openAILegacyUpstreamRateOrder {
rates := make(map[int64]float64, len(accounts))
var first float64
distinct := false
@@ -2890,11 +2890,19 @@ func newOpenAILegacyUpstreamRateOrder(accounts []*Account, now time.Time, oauthS
return openAILegacyUpstreamRateOrder{enabled: len(rates) >= 2 && distinct, rates: rates}
}
func openAISchedulingRate(account *Account, now time.Time, oauthSchedulingRateMultiplier float64) (float64, bool) {
if account != nil && account.IsOpenAIOAuthLike() {
return oauthSchedulingRateMultiplier, true
func openAISchedulingRate(account *Account, now time.Time, oauthSchedulingRateMultiplier *float64) (float64, bool) {
if account == nil || (!account.IsOpenAIApiKey() && !account.IsOpenAIOAuthLike()) {
return 0, false
}
return openAIFreshUpstreamBillingRate(account, now)
if account.IsOpenAIOAuthLike() {
if rate := oauthSchedulingRateMultiplier; rate != nil && *rate >= 0 && !math.IsNaN(*rate) && !math.IsInf(*rate, 0) {
return *rate, true
}
} else if rate, ok := openAIFreshUpstreamBillingRate(account, now); ok {
return rate, true
}
rate := account.BillingRateMultiplier()
return rate, !math.IsNaN(rate) && !math.IsInf(rate, 0)
}
// compare returns -1 when a should be selected before b, 1 when b should be
@@ -3,7 +3,6 @@ package service
import (
"context"
"errors"
"math"
"strconv"
"testing"
"time"
@@ -402,16 +401,17 @@ func TestOpenAIFreshUpstreamBillingRateRecomputesPeakAtSelectionTime(t *testing.
"timezone": "UTC",
}
duringPeak, ok := openAIFreshUpstreamBillingRate(account, time.Date(2026, 7, 13, 17, 59, 0, 0, time.UTC))
account.RateMultiplier = floatPtr(0.1)
duringPeak, ok := openAISchedulingRate(account, time.Date(2026, 7, 13, 17, 59, 0, 0, time.UTC), nil)
require.True(t, ok)
require.Equal(t, 0.8, duringPeak)
afterPeak, ok := openAIFreshUpstreamBillingRate(account, time.Date(2026, 7, 13, 18, 1, 0, 0, time.UTC))
afterPeak, ok := openAISchedulingRate(account, time.Date(2026, 7, 13, 18, 1, 0, 0, time.UTC), nil)
require.True(t, ok)
require.Equal(t, 0.4, afterPeak)
}
func TestOpenAIUpstreamCostFactorsSparseProbeIsNeutral(t *testing.T) {
func TestOpenAIUpstreamCostFactorsEqualProbeAndFallbackRatesAreNeutral(t *testing.T) {
now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
accounts := make([]*Account, 0, 10)
accounts = append(accounts, upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 1, now.Add(-time.Minute), 30*time.Minute))
@@ -430,13 +430,13 @@ func TestOpenAIUpstreamCostFactorsSparseProbeIsNeutral(t *testing.T) {
})
}
factors := openAIUpstreamCostFactors(accounts, now, defaultOpenAIOAuthSchedulingRateMultiplier)
factors := openAIUpstreamCostFactors(accounts, now, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
for id := int64(1); id <= 10; id++ {
require.Equal(t, openAIUpstreamCostNeutralFactor, factors[id])
}
}
func TestOpenAIUpstreamCostFactorsCoverageShrinksSparseSignal(t *testing.T) {
func TestOpenAIUpstreamCostFactorsIncludeAccountFallbacksInCoverage(t *testing.T) {
now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
accounts := []*Account{
upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute),
@@ -446,10 +446,10 @@ func TestOpenAIUpstreamCostFactorsCoverageShrinksSparseSignal(t *testing.T) {
accounts = append(accounts, &Account{ID: id, Platform: PlatformOpenAI, Type: AccountTypeAPIKey})
}
factors := openAIUpstreamCostFactors(accounts, now, defaultOpenAIOAuthSchedulingRateMultiplier)
center := math.Sqrt(0.03 * 0.8)
require.InDelta(t, 0.5+0.2*(1/(1+0.03/center)-0.5), factors[1], 1e-12)
require.InDelta(t, 0.5+0.2*(1/(1+0.8/center)-0.5), factors[2], 1e-12)
factors := openAIUpstreamCostFactors(accounts, now, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
// Eight unprobed accounts default to 1x, so the median is 1 and coverage is complete.
require.InDelta(t, 1/(1+0.03), factors[1], 1e-12)
require.InDelta(t, 1/(1+0.8), factors[2], 1e-12)
require.Equal(t, openAIUpstreamCostNeutralFactor, factors[3])
}
@@ -461,7 +461,7 @@ func TestOpenAIUpstreamCostFactorsUseMedianAgainstOutlier(t *testing.T) {
upstreamCostTestAccount(3, UpstreamBillingProbeStatusOK, 100, now.Add(-time.Minute), 30*time.Minute),
}
factors := openAIUpstreamCostFactors(accounts, now, defaultOpenAIOAuthSchedulingRateMultiplier)
factors := openAIUpstreamCostFactors(accounts, now, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
require.InDelta(t, 2.0/3.0, factors[1], 1e-12)
require.InDelta(t, 0.5, factors[2], 1e-12)
require.InDelta(t, 1/(1+100/0.2), factors[3], 1e-12)
@@ -472,20 +472,21 @@ func TestOpenAILegacyUpstreamRateOrderRequiresComparableRates(t *testing.T) {
oneKnown := newOpenAILegacyUpstreamRateOrder([]*Account{
upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute),
{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
}, now, defaultOpenAIOAuthSchedulingRateMultiplier)
require.False(t, oneKnown.enabled)
}, now, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
require.True(t, oneKnown.enabled)
require.Equal(t, 1.0, oneKnown.rates[2])
allEqual := newOpenAILegacyUpstreamRateOrder([]*Account{
upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.3, now.Add(-time.Minute), 30*time.Minute),
upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.3, now.Add(-time.Minute), 30*time.Minute),
}, now, defaultOpenAIOAuthSchedulingRateMultiplier)
}, now, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
require.False(t, allEqual.enabled)
distinct := newOpenAILegacyUpstreamRateOrder([]*Account{
upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute),
upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute),
{ID: 3, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
}, now, defaultOpenAIOAuthSchedulingRateMultiplier)
}, now, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
require.True(t, distinct.enabled)
require.Negative(t, distinct.compare(&Account{ID: 1}, &Account{ID: 2}))
require.Negative(t, distinct.compare(&Account{ID: 2}, &Account{ID: 3}))
@@ -506,12 +507,12 @@ func TestOpenAILegacyUpstreamRateOrderIgnoresNonOpenAIPlatforms(t *testing.T) {
grokCheap := nonOpenAI(1, PlatformGrok, 0.01)
anthropicExpensive := nonOpenAI(2, PlatformAnthropic, 0.9)
order := newOpenAILegacyUpstreamRateOrder([]*Account{grokCheap, anthropicExpensive, nil}, now, defaultOpenAIOAuthSchedulingRateMultiplier)
order := newOpenAILegacyUpstreamRateOrder([]*Account{grokCheap, anthropicExpensive, nil}, now, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
require.False(t, order.enabled)
require.Empty(t, order.rates)
require.Zero(t, order.compare(grokCheap, anthropicExpensive))
factors := openAIUpstreamCostFactors([]*Account{grokCheap, anthropicExpensive}, now, defaultOpenAIOAuthSchedulingRateMultiplier)
factors := openAIUpstreamCostFactors([]*Account{grokCheap, anthropicExpensive}, now, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
require.Equal(t, openAIUpstreamCostNeutralFactor, factors[grokCheap.ID])
require.Equal(t, openAIUpstreamCostNeutralFactor, factors[anthropicExpensive.ID])
@@ -520,7 +521,7 @@ func TestOpenAILegacyUpstreamRateOrderIgnoresNonOpenAIPlatforms(t *testing.T) {
openAIExpensive := upstreamCostTestAccount(4, UpstreamBillingProbeStatusOK, 0.12, now.Add(-time.Minute), 30*time.Minute)
mixed := newOpenAILegacyUpstreamRateOrder(
[]*Account{grokCheap, openAICheap, anthropicExpensive, openAIExpensive},
now, defaultOpenAIOAuthSchedulingRateMultiplier,
now, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier),
)
require.True(t, mixed.enabled)
require.Len(t, mixed.rates, 2)
@@ -537,12 +538,12 @@ func TestOpenAISchedulingRatePlacesOAuthAtConfiguredReference(t *testing.T) {
oauth := upstreamCostTestOAuthAccount(2)
expensive := upstreamCostTestAccount(3, UpstreamBillingProbeStatusOK, 0.12, now.Add(-time.Minute), 30*time.Minute)
order := newOpenAILegacyUpstreamRateOrder([]*Account{cheap, oauth, expensive}, now, 0.05)
order := newOpenAILegacyUpstreamRateOrder([]*Account{cheap, oauth, expensive}, now, floatPtr(0.05))
require.True(t, order.enabled)
require.Negative(t, order.compare(cheap, oauth))
require.Negative(t, order.compare(oauth, expensive))
factors := openAIUpstreamCostFactors([]*Account{cheap, oauth, expensive}, now, 0.05)
factors := openAIUpstreamCostFactors([]*Account{cheap, oauth, expensive}, now, floatPtr(0.05))
require.Greater(t, factors[cheap.ID], factors[oauth.ID])
require.Greater(t, factors[oauth.ID], factors[expensive.ID])
}
@@ -834,9 +835,9 @@ func TestBuildOpenAIAccountSchedulerScoreSnapshotUpstreamCostIsExactNoOpWithoutS
2: {AccountID: 2, LoadRate: 80},
}
weights := GatewayOpenAIWSSchedulerScoreWeightsView{Priority: 1, Load: 1, Queue: 0.7, ErrorRate: 0.8, TTFT: 0.5}
baseline := buildOpenAIAccountSchedulerScoreSnapshot(accounts, loadMap, weights, false, defaultOpenAIOAuthSchedulingRateMultiplier)
baseline := buildOpenAIAccountSchedulerScoreSnapshot(accounts, loadMap, weights, false, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
weights.UpstreamCost = 1.5
withCost := buildOpenAIAccountSchedulerScoreSnapshot(accounts, loadMap, weights, false, defaultOpenAIOAuthSchedulingRateMultiplier)
withCost := buildOpenAIAccountSchedulerScoreSnapshot(accounts, loadMap, weights, false, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
require.Equal(t, baseline, withCost)
}
@@ -848,7 +849,7 @@ func TestBuildOpenAIAccountSchedulerScoreSnapshotUsesUpstreamCostSignal(t *testi
upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute),
}
weights := GatewayOpenAIWSSchedulerScoreWeightsView{UpstreamCost: 1.5}
scores := buildOpenAIAccountSchedulerScoreSnapshot(accounts, nil, weights, false, defaultOpenAIOAuthSchedulingRateMultiplier)
scores := buildOpenAIAccountSchedulerScoreSnapshot(accounts, nil, weights, false, floatPtr(defaultOpenAIOAuthSchedulingRateMultiplier))
require.Greater(t, scores[1].BaseScore, scores[2].BaseScore)
}
@@ -0,0 +1,125 @@
package service
import (
"context"
"math"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestOpenAISchedulingRateFallback(t *testing.T) {
now := time.Now()
for _, tt := range []struct {
name string
status string
age time.Duration
probe float64
want float64
}{
{"fresh snapshot wins", UpstreamBillingProbeStatusOK, time.Minute, 0.6, 0.6},
{"fresh snapshot survives failed refresh", UpstreamBillingProbeStatusFailed, time.Minute, 0.6, 0.6},
{"expired snapshot falls back", UpstreamBillingProbeStatusOK, 3 * time.Hour, 0.6, 0.3},
{"future snapshot falls back", UpstreamBillingProbeStatusOK, -time.Hour, 0.6, 0.3},
{"invalid snapshot falls back", UpstreamBillingProbeStatusOK, time.Minute, -1, 0.3},
{"zero snapshot remains valid", UpstreamBillingProbeStatusOK, time.Minute, 0, 0},
} {
t.Run(tt.name, func(t *testing.T) {
account := upstreamCostTestAccount(1, tt.status, tt.probe, now.Add(-tt.age), time.Hour)
account.RateMultiplier = floatPtr(0.3)
rate, ok := openAISchedulingRate(account, now, floatPtr(0.9))
require.True(t, ok)
require.Equal(t, tt.want, rate)
})
}
for _, accountType := range []string{AccountTypeAPIKey, AccountTypeOAuth} {
t.Run(accountType+" without probe or override", func(t *testing.T) {
account := &Account{ID: 1, Platform: PlatformOpenAI, Type: accountType}
for _, tt := range []struct {
configured *float64
want float64
known bool
}{
{floatPtr(0.3), 0.3, true},
{floatPtr(0), 0, true},
{nil, 1, true},
{floatPtr(-1), 1, true},
{floatPtr(math.NaN()), 0, false},
{floatPtr(math.Inf(1)), 0, false},
} {
account.RateMultiplier = tt.configured
rate, ok := openAISchedulingRate(account, now, nil)
require.Equal(t, tt.known, ok)
if ok {
require.Equal(t, tt.want, rate)
}
}
})
}
oauth := upstreamCostTestOAuthAccount(1)
oauth.RateMultiplier = floatPtr(0.3)
for _, override := range []float64{0, 0.7} {
rate, ok := openAISchedulingRate(oauth, now, &override)
require.True(t, ok)
require.Equal(t, override, rate)
}
for _, override := range []float64{-1, math.NaN(), math.Inf(1)} {
rate, ok := openAISchedulingRate(oauth, now, &override)
require.True(t, ok)
require.Equal(t, 0.3, rate)
}
_, ok := openAISchedulingRate(nil, now, nil)
require.False(t, ok)
}
func TestOpenAISchedulingRateFallbackSharedByBothModes(t *testing.T) {
now := time.Now()
for _, accountType := range []string{AccountTypeAPIKey, AccountTypeOAuth} {
t.Run(accountType, func(t *testing.T) {
cheap := &Account{ID: 1, Platform: PlatformOpenAI, Type: accountType, RateMultiplier: floatPtr(0.3)}
expensive := &Account{ID: 2, Platform: PlatformOpenAI, Type: accountType, RateMultiplier: floatPtr(0.8)}
accounts := []*Account{cheap, expensive}
order := newOpenAILegacyUpstreamRateOrder(accounts, now, nil)
require.True(t, order.enabled)
require.Negative(t, order.compare(cheap, expensive))
factors := openAIUpstreamCostFactors(accounts, now, nil)
require.Greater(t, factors[cheap.ID], factors[expensive.ID])
if accountType == AccountTypeOAuth {
order = newOpenAILegacyUpstreamRateOrder(accounts, now, floatPtr(0.7))
require.False(t, order.enabled)
factors = openAIUpstreamCostFactors(accounts, now, floatPtr(0.7))
require.Equal(t, factors[cheap.ID], factors[expensive.ID])
}
})
}
}
func TestOpenAIOAuthSchedulingRateRuntimePreservesExplicitClear(t *testing.T) {
resetOpenAIAdvancedSchedulerSettingCacheForTest()
defer resetOpenAIAdvancedSchedulerSettingCacheForTest()
for _, tt := range []struct {
name string
values map[string]string
want *float64
}{
{"absent", map[string]string{}, floatPtr(1)},
{"cleared", map[string]string{SettingKeyOpenAIOAuthSchedulingRateMultiplier: ""}, nil},
{"zero", map[string]string{SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0"}, floatPtr(0)},
{"override", map[string]string{SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.7"}, floatPtr(0.7)},
} {
t.Run(tt.name, func(t *testing.T) {
resetOpenAIAdvancedSchedulerSettingCacheForTest()
svc := &OpenAIGatewayService{
rateLimitService: &RateLimitService{settingService: &SettingService{
settingRepo: &openAIAdvancedSchedulerSettingRepoStub{values: tt.values},
}},
}
for i := 0; i < 2; i++ {
require.Equal(t, tt.want, svc.openAIOAuthSchedulingRateMultiplier(context.Background()))
}
})
}
}
+11 -5
View File
@@ -915,7 +915,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
result.PaymentVisibleMethodAlipayEnabled = settings[SettingPaymentVisibleMethodAlipayEnabled] == "true"
result.PaymentVisibleMethodWxpayEnabled = settings[SettingPaymentVisibleMethodWxpayEnabled] == "true"
result.OpenAILowUpstreamRatePriorityEnabled = settings[SettingKeyOpenAILowUpstreamRatePriorityEnabled] == "true"
result.OpenAIOAuthSchedulingRateMultiplier = parseOpenAIOAuthSchedulingRateMultiplier(settings[SettingKeyOpenAIOAuthSchedulingRateMultiplier])
result.OpenAIOAuthSchedulingRateMultiplier = parseOpenAIOAuthSchedulingRateMultiplier(settings)
result.OpenAIAdvancedSchedulerEnabled = settings[openAIAdvancedSchedulerSettingKey] == "true"
result.OpenAIAdvancedSchedulerStickyWeightedEnabled = settings[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled] == "true"
result.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled = settings[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled] == "true"
@@ -1080,7 +1080,7 @@ func formatOpenAIAdvancedSchedulerFloat(value float64) string {
}
func (s *SettingService) normalizeOpenAIAdvancedSchedulerOverrides(settings *SystemSettings) error {
if rate := settings.OpenAIOAuthSchedulingRateMultiplier; rate < 0 || math.IsNaN(rate) || math.IsInf(rate, 0) {
if rate := settings.OpenAIOAuthSchedulingRateMultiplier; rate != nil && (*rate < 0 || math.IsNaN(*rate) || math.IsInf(*rate, 0)) {
return infraerrors.BadRequest("INVALID_OPENAI_OAUTH_SCHEDULING_RATE_MULTIPLIER", "OpenAI OAuth scheduling rate multiplier must be a finite non-negative number")
}
@@ -1132,12 +1132,18 @@ func (s *SettingService) normalizeOpenAIAdvancedSchedulerOverrides(settings *Sys
return nil
}
func parseOpenAIOAuthSchedulingRateMultiplier(raw string) float64 {
func parseOpenAIOAuthSchedulingRateMultiplier(settings map[string]string) *float64 {
raw, exists := settings[SettingKeyOpenAIOAuthSchedulingRateMultiplier]
if !exists {
// Preserve the legacy default until an administrator explicitly clears it.
value := defaultOpenAIOAuthSchedulingRateMultiplier
return &value
}
value, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
if err != nil || value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
return defaultOpenAIOAuthSchedulingRateMultiplier
return nil
}
return value
return &value
}
// resolveOpenAIAdvancedSchedulerWeight 返回覆盖值(已归一化的非空字符串),空则回退默认值。
@@ -410,7 +410,7 @@ func TestSettingService_UpdateSettings_PaymentVisibleMethodsAndAdvancedScheduler
PaymentVisibleMethodAlipayEnabled: true,
PaymentVisibleMethodWxpayEnabled: false,
OpenAILowUpstreamRatePriorityEnabled: true,
OpenAIOAuthSchedulingRateMultiplier: 0.05,
OpenAIOAuthSchedulingRateMultiplier: testPtrFloat64(0.05),
OpenAIAdvancedSchedulerEnabled: true,
OpenAIAdvancedSchedulerStickyWeightedEnabled: true,
OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: true,
@@ -454,7 +454,7 @@ func TestSettingService_UpdateSettingsRejectsInvalidOpenAIOAuthSchedulingRateMul
svc := NewSettingService(repo, &config.Config{})
for _, rate := range []float64{-0.01, math.NaN(), math.Inf(1)} {
err := svc.UpdateSettings(context.Background(), &SystemSettings{OpenAIOAuthSchedulingRateMultiplier: rate})
err := svc.UpdateSettings(context.Background(), &SystemSettings{OpenAIOAuthSchedulingRateMultiplier: &rate})
require.Error(t, err)
}
}
@@ -515,8 +515,12 @@ func TestSettingService_UpdateSettings_OpenAIAdvancedSchedulerWeightSums(t *test
func TestSettingService_ParseSettingsDefaultsOpenAIOAuthSchedulingRateMultiplier(t *testing.T) {
svc := NewSettingService(&settingUpdateRepoStub{}, &config.Config{})
require.Equal(t, 1.0, svc.parseSettings(map[string]string{}).OpenAIOAuthSchedulingRateMultiplier)
require.Equal(t, 0.05, svc.parseSettings(map[string]string{SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.05"}).OpenAIOAuthSchedulingRateMultiplier)
require.Equal(t, testPtrFloat64(1.0), svc.parseSettings(map[string]string{}).OpenAIOAuthSchedulingRateMultiplier)
require.Equal(t, testPtrFloat64(0.05), svc.parseSettings(map[string]string{SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.05"}).OpenAIOAuthSchedulingRateMultiplier)
require.Equal(t, testPtrFloat64(0), svc.parseSettings(map[string]string{SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0"}).OpenAIOAuthSchedulingRateMultiplier)
for _, raw := range []string{"", " ", "invalid", "-1", "NaN", "+Inf"} {
require.Nil(t, svc.parseSettings(map[string]string{SettingKeyOpenAIOAuthSchedulingRateMultiplier: raw}).OpenAIOAuthSchedulingRateMultiplier, raw)
}
}
func TestSettingService_GetAllSettings_OpenAIAdvancedSchedulerEffectiveValuesUseConfig(t *testing.T) {
+4 -1
View File
@@ -499,7 +499,10 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
updates[SettingPaymentVisibleMethodAlipayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodAlipayEnabled)
updates[SettingPaymentVisibleMethodWxpayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodWxpayEnabled)
updates[SettingKeyOpenAILowUpstreamRatePriorityEnabled] = strconv.FormatBool(settings.OpenAILowUpstreamRatePriorityEnabled)
updates[SettingKeyOpenAIOAuthSchedulingRateMultiplier] = strconv.FormatFloat(settings.OpenAIOAuthSchedulingRateMultiplier, 'f', -1, 64)
updates[SettingKeyOpenAIOAuthSchedulingRateMultiplier] = ""
if rate := settings.OpenAIOAuthSchedulingRateMultiplier; rate != nil {
updates[SettingKeyOpenAIOAuthSchedulingRateMultiplier] = strconv.FormatFloat(*rate, 'f', -1, 64)
}
updates[openAIAdvancedSchedulerSettingKey] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerEnabled)
updates[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerStickyWeightedEnabled)
updates[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled)
+1 -1
View File
@@ -268,7 +268,7 @@ type SystemSettings struct {
// OpenAI 账号调度
OpenAILowUpstreamRatePriorityEnabled bool
OpenAIOAuthSchedulingRateMultiplier float64
OpenAIOAuthSchedulingRateMultiplier *float64
OpenAIAdvancedSchedulerEnabled bool
OpenAIAdvancedSchedulerStickyWeightedEnabled bool
OpenAIAdvancedSchedulerSubscriptionPriorityEnabled bool