mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-27 16:07:02 +00:00
审计修复,逐条如下。 H1 利润策略泄露给所有普通用户 profit_control_enabled / profit_min_margin / profit_safety_buffer 从 dto.Group 移到 dto.AdminGroup(后者内嵌前者),赋值相应从 groupFromServiceBase 移到 GroupFromServiceAdmin;前端 TS 同步从 Group 移到 AdminGroup。dto.Group 是 GET /api/v1/groups/available 的响应体,该响应本就带 rate_multiplier,相乘即可反推运营方上游采购成本上限。 api_contract_test.go 的 /groups/available golden JSON 回滚这三个字段,并把 fixture 改成非零值(require.JSONEq 是精确比对,缺字段即失败)。 新增 dto 层边界测试:普通用户 DTO 不含三字段、管理员 DTO 仍含。 M1 利润终检 continue 与 failover 503 退避互动产生活锁 FailoverState 新增 profitVetoedAccountIDs / profitVetoCount 与 RecordProfitVeto():加入排除集 + 计数,达 maxProfitVetoAttempts(10) 返回 FailoverExhausted。HandleSelectionExhausted 的 503 清空分支改为清空后把利润 否决的账号放回排除集;若排除集已全部由利润否决贡献,清空不会带来任何新候选, 直接判定耗尽(否则 SwitchCount 永不前进、退避条件永远成立,每 2s 空转一轮)。 五个 handler 否决点(gateway_handler ×2 / responses / chat_completions / gemini_v1beta)改为经 RecordProfitVeto 决策,耗尽时按无可用账号终止。 回归测试钉死:503 之后持续利润否决必须有限步终止且不 spin;未启用利润控制的 请求退避语义完全不变。 M2 排队等槽后才终检,延迟可放大到 N × WaitPlan.Timeout OpenAI 侧选号循环(自有 failedAccountIDs map,非 FailoverState)新增 recordOpenAIProfitVeto + handleOpenAIProfitVetoExhausted,共用同一上限语义。 覆盖 responses / messages-dispatch / chat_completions / alpha_search / embeddings / images / grok_media 七处,以及 WS 两处否决分支。 M4 ws_v2 透传 ingress 绕过 per-turn 重定价(选方案 B:最小止血) 透传 relay 只回调 AfterTurn、没有任何 turn 起始回调,hooks.BeforeTurn 永远 不触发,而 handler 把 turnPricingAt 初始化成建连时刻 ⇒ 透传连接全部 turn 按 建连时刻的高峰因子结算,客户端峰前建连保活即可全程谷价——正是本 PR 想堵的 漏洞。改为 openAIWSTurnPricing 零值起步、只由 BeforeTurn 冻结;透传路径保持 零值,RecordUsage 回退记录时刻,与引入利润控制前的基线一致。 未选方案 A(给透传补 turn 起始回调):passthrough_relay.go 是 #5167 刚修过的 取消传播/close frame 时序敏感区;且 BeforeTurn 还承担 turn>1 的并发槽位抢占, 接进去等于给透传连接引入 per-turn 抢槽,风险远超本次修复范围。透传仍有建连时 的准入门,只是没有 turn 级复核,已在两处注释写明。 测试:service 层钉死透传 ingress 不触发 BeforeTurn(含失败时的复核指引), handler 层钉死零值语义与逐 turn 覆盖。 M5 装门读分组走了带账号计数聚合的 GetByID SchedulerSnapshotService 新增 GetGroupByIDLite,openai/gateway 两处装门改用 之。门只需要平台/倍率/利润/高峰字段,且该查询发生在「是否启用利润控制」判定 之前,未启用的分组同样付代价。两个测试 stub 的 GetByID 改成 panic 守卫。 M6 认证快照注释与真实读取路径相反 门解析优先取 ctxkey.Group,而它就是本快照物化出来的对象,直连流量走的正是这 条路。改正注释,与 api_key_repo.go 投影处的说明对齐,避免后人照旧注释删列。 M3 rate_multiplier 为 nil 时利润门 fail-closed(不改行为,加护栏) 保留 fail-closed。补 repository 层测试钉死账号调度快照的 full/metadata 两份 payload 都必须保留 RateMultiplier(含 0 值),漏列在 CI 就红。 L1 迁移号注释 191 / 191-192 改为实际的 192/193。 L2 admin group Create 的利润配置预校验改用与 CreateGroup 一致的归一化平台 (新增 service.NormalizeGroupPlatform,两边共用)。保留预校验而非删除: service 层返回的是无类型 error,经 ErrorFrom 会变成 500,删掉会把合法的 400 降级成 500。 L3 前端利润校验的上界改为判定换算后的小数(后端按小数校验 [0,1)), 99.999% 会四舍五入进位成 1.0 而被后端 400;i18n en/zh 同步改为 0-99.99。 L4 clampProfitControlThreshold / profitControlOverThreshold 抽为共用函数, 线上装门/否决点与 profit-preview 不再各自实现,附边界语义测试。 L5 profit-preview 补「默认 D 有账号但最低有效 D 归零」的告警(两档都为 0 由 既有告警覆盖,不重复)。
417 lines
15 KiB
Go
417 lines
15 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"math"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||
)
|
||
|
||
type OpenAIMessagesDispatchModelConfig = domain.OpenAIMessagesDispatchModelConfig
|
||
type GroupModelsListConfig = domain.GroupModelsListConfig
|
||
type ReasoningEffortMapping = domain.ReasoningEffortMapping
|
||
|
||
type Group struct {
|
||
ID int64
|
||
Name string
|
||
Description string
|
||
Platform string
|
||
RateMultiplier float64
|
||
// 高峰时段倍率:peak_rate_enabled 为 true 且当前时刻处于 [PeakStart, PeakEnd) 时,
|
||
// token 计费倍率额外乘以 PeakRateMultiplier。详见 PeakMultiplierAt。
|
||
PeakRateEnabled bool
|
||
PeakStart string
|
||
PeakEnd string
|
||
PeakRateMultiplier float64
|
||
IsExclusive bool
|
||
Status string
|
||
Hydrated bool // indicates the group was loaded from a trusted repository source
|
||
// DuplicateOperationID is internal persistence metadata used only to recover
|
||
// an already committed one-click copy. It must never be mapped to API DTOs.
|
||
DuplicateOperationID string
|
||
|
||
SubscriptionType string
|
||
DailyLimitUSD *float64
|
||
WeeklyLimitUSD *float64
|
||
MonthlyLimitUSD *float64
|
||
DefaultValidityDays int
|
||
|
||
// 图片生成计费配置(antigravity 和 gemini 平台使用)
|
||
AllowImageGeneration bool
|
||
AllowBatchImageGeneration bool
|
||
ImageRateIndependent bool
|
||
ImageRateMultiplier float64
|
||
ImagePrice1K *float64
|
||
ImagePrice2K *float64
|
||
ImagePrice4K *float64
|
||
BatchImageDiscountMultiplier float64
|
||
BatchImageHoldMultiplier float64
|
||
VideoRateIndependent bool
|
||
VideoRateMultiplier float64
|
||
VideoPrice480P *float64
|
||
VideoPrice720P *float64
|
||
VideoPrice1080P *float64
|
||
// Codex alpha/search 网页搜索单次价格(USD/次,仅 openai 平台使用);
|
||
// nil 表示使用默认价 defaultWebSearchPricePerCall(官方 $10/1000 次)。
|
||
WebSearchPricePerCall *float64
|
||
|
||
// Claude Code 客户端限制
|
||
ClaudeCodeOnly bool
|
||
FallbackGroupID *int64
|
||
// 无效请求兜底分组(仅 anthropic 平台使用)
|
||
FallbackGroupIDOnInvalidRequest *int64
|
||
|
||
// 模型路由配置
|
||
// key: 模型匹配模式(支持 * 通配符,如 "claude-opus-*")
|
||
// value: 优先账号 ID 列表
|
||
ModelRouting map[string][]int64
|
||
ModelRoutingEnabled bool
|
||
|
||
// MCP XML 协议注入开关(仅 antigravity 平台使用)
|
||
MCPXMLInject bool
|
||
|
||
// 支持的模型系列(仅 antigravity 平台使用)
|
||
// 可选值: claude, gemini_text, gemini_image
|
||
SupportedModelScopes []string
|
||
|
||
// 分组排序
|
||
SortOrder int
|
||
|
||
// OpenAI Messages 调度配置(仅 openai 平台使用)
|
||
AllowMessagesDispatch bool
|
||
AllowLive bool
|
||
RequireOAuthOnly bool // 仅允许非 apikey 类型账号关联(OpenAI/Antigravity/Anthropic/Gemini)
|
||
RequirePrivacySet bool // 调度时仅允许 privacy 已成功设置的账号(OpenAI/Antigravity/Anthropic/Gemini)
|
||
DefaultMappedModel string
|
||
MessagesDispatchModelConfig OpenAIMessagesDispatchModelConfig
|
||
ModelsListConfig GroupModelsListConfig
|
||
|
||
// RPMLimit 分组级每分钟请求数上限(0 = 不限制)。
|
||
// 一旦设置即接管该分组用户的限流(覆盖用户级 rpm_limit),可被 user-group rpm_override 进一步覆盖。
|
||
RPMLimit int
|
||
|
||
// MaxReasoningEffort limits the effective OpenAI/Codex reasoning effort.
|
||
// Empty means unlimited; supported values are minimal/low/medium/high/xhigh/max.
|
||
MaxReasoningEffort string
|
||
// ReasoningEffortMappings rewrites explicit request values before applying the ceiling.
|
||
ReasoningEffortMappings []ReasoningEffortMapping
|
||
|
||
// 分组利润控制(五个 token 计费平台可启用)。
|
||
// 调度准入条件:账号倍率 U 满足 U <= D*(1-margin-buffer),
|
||
// D 为请求用户当刻有效下游倍率(用户覆盖 ?? 分组默认,再乘高峰因子)。
|
||
// 只过滤候选账号,不改变既有排序/评分/粘性/熔断。
|
||
ProfitControlEnabled bool
|
||
ProfitMinMargin float64 // 最低毛利率,小数存储(0.30=30%)
|
||
ProfitSafetyBuffer float64 // 安全缓冲,小数,与 margin 相加后从 D 中扣除
|
||
|
||
CreatedAt time.Time
|
||
UpdatedAt time.Time
|
||
|
||
AccountGroups []AccountGroup
|
||
AccountCount int64
|
||
ActiveAccountCount int64
|
||
RateLimitedAccountCount int64
|
||
}
|
||
|
||
func (g *Group) IsActive() bool {
|
||
return g.Status == StatusActive
|
||
}
|
||
|
||
func (g *Group) IsSubscriptionType() bool {
|
||
return g.SubscriptionType == SubscriptionTypeSubscription
|
||
}
|
||
|
||
func (g *Group) HasDailyLimit() bool {
|
||
return g.DailyLimitUSD != nil && *g.DailyLimitUSD > 0
|
||
}
|
||
|
||
func (g *Group) HasWeeklyLimit() bool {
|
||
return g.WeeklyLimitUSD != nil && *g.WeeklyLimitUSD > 0
|
||
}
|
||
|
||
func (g *Group) HasMonthlyLimit() bool {
|
||
return g.MonthlyLimitUSD != nil && *g.MonthlyLimitUSD > 0
|
||
}
|
||
|
||
// GetImagePrice 根据 image_size 返回对应的图片生成价格
|
||
// 如果分组未配置价格,返回 nil(调用方应使用默认值)
|
||
func (g *Group) GetImagePrice(imageSize string) *float64 {
|
||
switch imageSize {
|
||
case "1K":
|
||
return g.ImagePrice1K
|
||
case "2K":
|
||
return g.ImagePrice2K
|
||
case "4K":
|
||
return g.ImagePrice4K
|
||
default:
|
||
// 未知尺寸默认按 2K 计费
|
||
return g.ImagePrice2K
|
||
}
|
||
}
|
||
|
||
// GetVideoPrice 根据 resolution 返回对应的视频生成价格。
|
||
// 如果分组未配置价格,返回 nil(调用方应使用默认值)。
|
||
func (g *Group) GetVideoPrice(resolution string) *float64 {
|
||
switch NormalizeVideoBillingResolutionOrDefault(resolution) {
|
||
case VideoBillingResolution480P:
|
||
return g.VideoPrice480P
|
||
case VideoBillingResolution720P:
|
||
return g.VideoPrice720P
|
||
case VideoBillingResolution1080P:
|
||
return g.VideoPrice1080P
|
||
default:
|
||
return g.VideoPrice480P
|
||
}
|
||
}
|
||
|
||
// IsGroupContextValid reports whether a group from context has the fields required for routing decisions.
|
||
func IsGroupContextValid(group *Group) bool {
|
||
if group == nil {
|
||
return false
|
||
}
|
||
if group.ID <= 0 {
|
||
return false
|
||
}
|
||
if !group.Hydrated {
|
||
return false
|
||
}
|
||
if group.Platform == "" || group.Status == "" {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// GetRoutingAccountIDs 根据请求模型获取路由账号 ID 列表
|
||
// 返回匹配的优先账号 ID 列表,如果没有匹配规则则返回 nil
|
||
func (g *Group) GetRoutingAccountIDs(requestedModel string) []int64 {
|
||
if !g.ModelRoutingEnabled || len(g.ModelRouting) == 0 || requestedModel == "" {
|
||
return nil
|
||
}
|
||
|
||
// 1. 精确匹配优先
|
||
if accountIDs, ok := g.ModelRouting[requestedModel]; ok && len(accountIDs) > 0 {
|
||
return accountIDs
|
||
}
|
||
|
||
// 2. 通配符匹配(前缀匹配)
|
||
for pattern, accountIDs := range g.ModelRouting {
|
||
if matchModelPattern(pattern, requestedModel) && len(accountIDs) > 0 {
|
||
return accountIDs
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// matchModelPattern 检查模型是否匹配模式
|
||
// 支持 * 通配符,如 "claude-opus-*" 匹配 "claude-opus-4-20250514"
|
||
func matchModelPattern(pattern, model string) bool {
|
||
if pattern == model {
|
||
return true
|
||
}
|
||
|
||
// 处理 * 通配符(仅支持末尾通配符)
|
||
if strings.HasSuffix(pattern, "*") {
|
||
prefix := strings.TrimSuffix(pattern, "*")
|
||
return strings.HasPrefix(model, prefix)
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// parseMinutes 把 "HH:MM" 解析为当日分钟数(0..1439),格式非法返回 (0,false)。
|
||
// 手工解析而非 time.Parse:本函数位于每请求的计费热路径(PeakMultiplierAt),
|
||
// 避免对静态配置字符串重复走 layout 解析与 time.Time 分配。
|
||
// 接受集与 time.Parse("15:04", s) 完全一致(存量数据按旧解析写入,不得收窄):
|
||
// 小时 1–2 位数字(0..23,允许不补零如 "1:30"),分钟固定 2 位数字(00..59)。
|
||
func parseMinutes(hhmm string) (int, bool) {
|
||
colon := strings.IndexByte(hhmm, ':')
|
||
if (colon != 1 && colon != 2) || len(hhmm)-colon-1 != 2 {
|
||
return 0, false
|
||
}
|
||
h := 0
|
||
for i := 0; i < colon; i++ {
|
||
d := hhmm[i] - '0'
|
||
if d > 9 {
|
||
return 0, false
|
||
}
|
||
h = h*10 + int(d)
|
||
}
|
||
m1, m2 := hhmm[colon+1]-'0', hhmm[colon+2]-'0'
|
||
if m1 > 9 || m2 > 9 {
|
||
return 0, false
|
||
}
|
||
m := int(m1)*10 + int(m2)
|
||
if h > 23 || m > 59 {
|
||
return 0, false
|
||
}
|
||
return h*60 + m, true
|
||
}
|
||
|
||
// PeakMultiplierAt 返回指定时刻 now 的高峰因子。
|
||
// - 未启用 / 未配置 / 配置非法(start>=end 或格式错误) / 非高峰时段 → 返回 1.0(安全降级)
|
||
// - 区间为左闭右开 [PeakStart, PeakEnd),仅支持当日区间,不支持跨天(如 22:00-次日02:00)
|
||
// - 时刻基于全局系统时区(timezone.Location)判定
|
||
//
|
||
// 该方法是纯函数,不读取任何外部状态,便于单测。
|
||
func (g *Group) PeakMultiplierAt(now time.Time) float64 {
|
||
if g == nil || !g.IsSubscriptionType() || !g.PeakRateEnabled || g.PeakStart == "" || g.PeakEnd == "" {
|
||
return 1.0
|
||
}
|
||
start, ok1 := parseMinutes(g.PeakStart)
|
||
end, ok2 := parseMinutes(g.PeakEnd)
|
||
if !ok1 || !ok2 || start >= end {
|
||
return 1.0
|
||
}
|
||
t := now.In(timezone.Location())
|
||
cur := t.Hour()*60 + t.Minute()
|
||
if cur >= start && cur < end {
|
||
return g.PeakRateMultiplier
|
||
}
|
||
return 1.0
|
||
}
|
||
|
||
// ValidatePeakRateConfig 是高峰倍率配置的唯一校验来源,供 handler 与 service 层共用。
|
||
// enabled=true 时仅允许订阅类型分组;并要求 start/end 合法且 end>start(不支持跨天),multiplier>=0。
|
||
// multiplier=0 是允许的,表示高峰 token 请求按 0 倍计费,可用于折扣/免费策略。
|
||
// enabled=false 时放行(不关心类型)。subscriptionType 为空按 standard 处理。
|
||
func ValidatePeakRateConfig(subscriptionType string, enabled bool, start, end string, multiplier float64) error {
|
||
if !enabled {
|
||
return nil
|
||
}
|
||
if subscriptionType != SubscriptionTypeSubscription {
|
||
return errors.New("高峰时段倍率仅支持订阅类型分组")
|
||
}
|
||
if start == "" || end == "" {
|
||
return errors.New("peak_rate_enabled 为 true 时 peak_start 与 peak_end 必填")
|
||
}
|
||
st, okStart := parseMinutes(start)
|
||
if !okStart {
|
||
return fmt.Errorf("peak_start 格式应为 HH:MM,got %q", start)
|
||
}
|
||
en, okEnd := parseMinutes(end)
|
||
if !okEnd {
|
||
return fmt.Errorf("peak_end 格式应为 HH:MM,got %q", end)
|
||
}
|
||
if st >= en {
|
||
return errors.New("peak_end 必须大于 peak_start(不支持跨天区间,如 22:00-02:00)")
|
||
}
|
||
if multiplier < 0 {
|
||
return errors.New("peak_rate_multiplier 不能为负")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// NormalizePeakRateConfig 归一化最终落库的高峰配置,CreateGroup 与 UpdateGroup 两条写路径共用(唯一收口):
|
||
// - 非订阅类型分组不携带任何高峰配置,一律清空(enabled=false、窗口置空、倍率归 1.0);
|
||
// - 订阅分组关闭高峰时保留已配置的合法窗口(便于临时停用后再启用),
|
||
// 但清掉无法解析的脏字符串与负倍率,避免脏数据入库。
|
||
//
|
||
// 与 ValidatePeakRateConfig 的分工:enabled=true 时校验已保证各字段合法,本函数为无操作;
|
||
// enabled=false 时校验放行,由本函数兜底清洗。调用顺序为先归一化、后校验,
|
||
// 使"订阅转标准"这类更新能静默清空高峰配置而不是被校验拒绝。
|
||
func NormalizePeakRateConfig(subscriptionType string, enabled bool, start, end string, multiplier float64) (bool, string, string, float64) {
|
||
if subscriptionType != SubscriptionTypeSubscription {
|
||
return false, "", "", 1.0
|
||
}
|
||
if !enabled {
|
||
if _, ok := parseMinutes(start); !ok {
|
||
start = ""
|
||
}
|
||
if _, ok := parseMinutes(end); !ok {
|
||
end = ""
|
||
}
|
||
if multiplier < 0 {
|
||
multiplier = 1.0
|
||
}
|
||
}
|
||
return enabled, start, end, multiplier
|
||
}
|
||
|
||
// computePeakAwareMultipliers 把"基础 token 倍率 base"(已含系统/分组/用户级倍率,但不含高峰)
|
||
// 拆分为最终 token 倍率与图片按次倍率:图片按次倍率基于 base 现算、不受高峰影响;token 倍率在 base 上叠加高峰因子。
|
||
// gateway_service.recordUsageCore 与 openai_gateway_service.RecordUsage 共用此函数,
|
||
// 锁死"高峰因子只乘入 token 倍率、图片按次倍率不受影响"这一叠加顺序——任何调换都会被 group_peak_rate_test 覆盖。
|
||
func computePeakAwareMultipliers(apiKey *APIKey, base float64, now time.Time) (text, image float64) {
|
||
image = resolveImageRateMultiplier(apiKey, base)
|
||
peak := 1.0
|
||
if apiKey != nil && apiKey.Group != nil {
|
||
peak = apiKey.Group.PeakMultiplierAt(now)
|
||
}
|
||
text = base * peak
|
||
return
|
||
}
|
||
|
||
// validProfitControlRatio 判定 margin/buffer 是否为可落库的合法小数:[0,1) 且非 NaN/Inf。
|
||
func validProfitControlRatio(v float64) bool {
|
||
return !math.IsNaN(v) && !math.IsInf(v, 0) && v >= 0 && v < 1
|
||
}
|
||
|
||
// NormalizeGroupPlatform 把创建分组时省略的 platform 归一化为默认平台。
|
||
// handler 的入参预校验必须与 CreateGroup 落库时用同一个归一化结果,否则
|
||
// 「省略 platform + 启用利润控制」会被 handler 以「平台不支持」400 掉,
|
||
// 而该分组本会被建成受支持的 anthropic 分组。
|
||
func NormalizeGroupPlatform(platform string) string {
|
||
if platform == "" {
|
||
return PlatformAnthropic
|
||
}
|
||
return platform
|
||
}
|
||
|
||
// ValidateProfitControlConfig 是分组利润控制配置的唯一校验来源,handler 与 service 层共用。
|
||
// enabled=true 时仅允许五个可计费平台分组;margin/buffer 各自 ∈ [0,1),且 margin+buffer < 1
|
||
// (相加 >=1 时阈值 <=0,所有可核价账号都会被排除,视为配置错误而不是静默全黑)。
|
||
// enabled=false 时放行(不关心平台),由 Normalize 兜底清洗数值。
|
||
func ValidateProfitControlConfig(platform string, enabled bool, minMargin, safetyBuffer float64) error {
|
||
if !enabled {
|
||
return nil
|
||
}
|
||
if !profitControlPlatformSupported(platform) {
|
||
return errors.New("利润控制仅支持 openai、anthropic、gemini、grok、antigravity 平台分组")
|
||
}
|
||
if !validProfitControlRatio(minMargin) {
|
||
return fmt.Errorf("profit_min_margin 应为 [0,1) 的小数,got %v", minMargin)
|
||
}
|
||
if !validProfitControlRatio(safetyBuffer) {
|
||
return fmt.Errorf("profit_safety_buffer 应为 [0,1) 的小数,got %v", safetyBuffer)
|
||
}
|
||
if minMargin+safetyBuffer >= 1 {
|
||
return errors.New("profit_min_margin 与 profit_safety_buffer 之和必须小于 1,否则将排除全部账号")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// NormalizeProfitControlConfig 归一化最终落库的利润控制配置,CreateGroup 与 UpdateGroup 共用(唯一收口):
|
||
// - 非五个平台分组不携带利润控制,一律重置为默认(关、0、0);
|
||
// - 支持平台关闭开关时保留合法数值(便于再次启用),清洗 NaN/Inf/越界脏值。
|
||
//
|
||
// 与 ValidateProfitControlConfig 的分工同高峰倍率:先归一化、后校验,
|
||
// 使"openai 转其他平台"这类更新能静默清空利润配置而不是被校验拒绝。
|
||
func NormalizeProfitControlConfig(platform string, enabled bool, minMargin, safetyBuffer float64) (bool, float64, float64) {
|
||
if !profitControlPlatformSupported(platform) {
|
||
return false, 0, 0
|
||
}
|
||
if !enabled {
|
||
if !validProfitControlRatio(minMargin) {
|
||
minMargin = 0
|
||
}
|
||
if !validProfitControlRatio(safetyBuffer) {
|
||
safetyBuffer = 0
|
||
}
|
||
}
|
||
return enabled, minMargin, safetyBuffer
|
||
}
|
||
|
||
func profitControlPlatformSupported(platform string) bool {
|
||
switch platform {
|
||
case PlatformOpenAI, PlatformAnthropic, PlatformGemini, PlatformGrok, PlatformAntigravity:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|