| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055 |
- package service
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "log/slog"
- "strconv"
- "strings"
- "sync/atomic"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
- "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
- "golang.org/x/sync/singleflight"
- )
- // cachedVersionBounds 缓存 Claude Code 版本号上下限(进程内缓存,60s TTL)
- type cachedVersionBounds struct {
- min string // 空字符串 = 不检查
- max string // 空字符串 = 不检查
- expiresAt int64 // unix nano
- }
- // versionBoundsCache 版本号上下限进程内缓存
- var versionBoundsCache atomic.Value // *cachedVersionBounds
- // versionBoundsSF 防止缓存过期时 thundering herd
- var versionBoundsSF singleflight.Group
- // versionBoundsCacheTTL 缓存有效期
- const versionBoundsCacheTTL = 60 * time.Second
- // versionBoundsErrorTTL DB 错误时的短缓存,快速重试
- const versionBoundsErrorTTL = 5 * time.Second
- // versionBoundsDBTimeout singleflight 内 DB 查询超时,独立于请求 context
- const versionBoundsDBTimeout = 5 * time.Second
- // cachedBackendMode Backend Mode cache (in-process, 60s TTL)
- type cachedBackendMode struct {
- value bool
- expiresAt int64 // unix nano
- }
- var backendModeCache atomic.Value // *cachedBackendMode
- var backendModeSF singleflight.Group
- const backendModeCacheTTL = 60 * time.Second
- const backendModeErrorTTL = 5 * time.Second
- const backendModeDBTimeout = 5 * time.Second
- // cachedGatewayForwardingSettings 缓存网关转发行为设置(进程内缓存,60s TTL)
- type cachedGatewayForwardingSettings struct {
- openAITTFTMode string
- fingerprintUnification bool
- metadataPassthrough bool
- cchSigning bool
- claudeOAuthSystemPromptInjection bool
- claudeOAuthSystemPrompt string
- claudeOAuthSystemPromptBlocks string
- anthropicCacheTTL1hInjection bool
- rewriteMessageCacheControl bool
- clientDatelineNormalization bool
- expiresAt int64 // unix nano
- }
- var gatewayForwardingCache atomic.Value // *cachedGatewayForwardingSettings
- var gatewayForwardingSF singleflight.Group
- const gatewayForwardingCacheTTL = 60 * time.Second
- const gatewayForwardingErrorTTL = 5 * time.Second
- const gatewayForwardingDBTimeout = 5 * time.Second
- // cachedAccountSchedulingThresholds 缓存平台自动停调阈值(进程内缓存,60s TTL)
- type cachedAccountSchedulingThresholds struct {
- thresholds map[string]int
- expiresAt int64 // unix nano
- }
- var accountSchedulingThresholdsCache atomic.Value // *cachedAccountSchedulingThresholds
- var accountSchedulingThresholdsSF singleflight.Group
- const accountSchedulingThresholdsCacheTTL = 60 * time.Second
- const accountSchedulingThresholdsErrorTTL = 5 * time.Second
- const accountSchedulingThresholdsDBTimeout = 5 * time.Second
- // cachedAntigravityUserAgentVersion 缓存 Antigravity UA 版本号(进程内缓存,60s TTL)
- type cachedAntigravityUserAgentVersion struct {
- version string
- expiresAt int64 // unix nano
- }
- const antigravityUserAgentVersionCacheTTL = 60 * time.Second
- const antigravityUserAgentVersionErrorTTL = 5 * time.Second
- const antigravityUserAgentVersionDBTimeout = 5 * time.Second
- // DefaultOpenAICodexUserAgent 是 OpenAI Codex 默认 User-Agent,用于规避浏览器 UA 的质询。
- // 默认采用 codex-tui 身份,版本段随 codexCLIVersion 一起更新。
- const DefaultOpenAICodexUserAgent = codexCLIUserAgent
- // cachedOpenAICodexUserAgent 缓存 OpenAI Codex UA(进程内缓存,60s TTL)
- type cachedOpenAICodexUserAgent struct {
- value string
- expiresAt int64 // unix nano
- }
- // cachedOpenAICodexClientVersion 缓存出站 Codex 客户端版本号(进程内缓存,60s TTL)
- type cachedOpenAICodexClientVersion struct {
- version string
- expiresAt int64 // unix nano
- }
- const openAICodexClientVersionCacheTTL = 60 * time.Second
- const openAICodexClientVersionErrorTTL = 5 * time.Second
- const openAICodexClientVersionDBTimeout = 5 * time.Second
- // openAICodexClientVersionSFKey singleflight 键。
- const openAICodexClientVersionSFKey = "openai_codex_client_version"
- type cachedOpenAIQuotaAutoPauseSettings struct {
- settings OpsOpenAIAccountQuotaAutoPauseSettings
- expiresAt int64
- }
- const openAICodexUserAgentCacheTTL = 60 * time.Second
- const openAICodexUserAgentErrorTTL = 5 * time.Second
- const openAICodexUserAgentDBTimeout = 5 * time.Second
- const codexRestrictionPolicyCacheTTL = 60 * time.Second
- const codexRestrictionPolicyDBTimeout = 5 * time.Second
- // cachedCodexRestrictionPolicy codex_cli_only 全局加固策略缓存(进程内,60s TTL)。
- // GetCodexRestrictionPolicy 在每个 codex_cli_only 账号的网关请求热路径上被调用,避免每次访问 DB。
- type cachedCodexRestrictionPolicy struct {
- value CodexRestrictionPolicy
- expiresAt int64 // unix nano
- }
- // cachedCyberSessionBlockRuntime cyber 会话屏蔽开关+TTL 进程内缓存(60s TTL)。
- // GetCyberSessionBlockRuntime 在网关请求热路径上被调用,避免每次访问 DB。
- type cachedCyberSessionBlockRuntime struct {
- enabled bool
- ttl time.Duration
- expiresAt int64 // unix nano
- }
- const cyberSessionBlockRuntimeCacheTTL = 60 * time.Second
- const cyberSessionBlockRuntimeErrorTTL = 5 * time.Second
- const cyberSessionBlockRuntimeDBTimeout = 5 * time.Second
- const openAIQuotaAutoPauseSettingsCacheTTL = 60 * time.Second
- const openAIQuotaAutoPauseSettingsErrorTTL = 5 * time.Second
- const openAIQuotaAutoPauseSettingsDBTimeout = 5 * time.Second
- const openAIQuotaAutoPauseSettingsRefreshKey = "openai_quota_auto_pause_settings"
- // GetCyberSessionBlockRuntime 返回 (开关, TTL),进程内缓存 ~60s,
- // 供网关热路径读取时避免 DB 往返。
- // 两个 setting key 在单次 singleflight 里一起读取,减少 DB 往返。
- // 默认值:开关 false,TTL 1h(与粘性会话对齐)。
- func (s *SettingService) GetCyberSessionBlockRuntime(ctx context.Context) (bool, time.Duration) {
- if cached, ok := s.cyberSessionBlockRuntimeCache.Load().(*cachedCyberSessionBlockRuntime); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.enabled, cached.ttl
- }
- }
- result, _, _ := s.cyberSessionBlockRuntimeSF.Do("cyber_session_block_runtime", func() (any, error) {
- if cached, ok := s.cyberSessionBlockRuntimeCache.Load().(*cachedCyberSessionBlockRuntime); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached, nil
- }
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), cyberSessionBlockRuntimeDBTimeout)
- defer cancel()
- enabledVal, enabledErr := s.settingRepo.GetValue(dbCtx, SettingKeyCyberSessionBlockEnabled)
- ttlVal, ttlErr := s.settingRepo.GetValue(dbCtx, SettingKeyCyberSessionBlockTTLSeconds)
- if enabledErr != nil && !errors.Is(enabledErr, ErrSettingNotFound) {
- slog.Warn("failed to get cyber_session_block_enabled setting", "error", enabledErr)
- entry := &cachedCyberSessionBlockRuntime{
- enabled: false,
- ttl: time.Hour,
- expiresAt: time.Now().Add(cyberSessionBlockRuntimeErrorTTL).UnixNano(),
- }
- s.cyberSessionBlockRuntimeCache.Store(entry)
- return entry, nil
- }
- enabled := enabledErr == nil && strings.TrimSpace(enabledVal) == "true"
- ttl := time.Hour
- if ttlErr == nil {
- if n, perr := strconv.Atoi(strings.TrimSpace(ttlVal)); perr == nil && n > 0 {
- ttl = time.Duration(n) * time.Second
- }
- }
- entry := &cachedCyberSessionBlockRuntime{
- enabled: enabled,
- ttl: ttl,
- expiresAt: time.Now().Add(cyberSessionBlockRuntimeCacheTTL).UnixNano(),
- }
- s.cyberSessionBlockRuntimeCache.Store(entry)
- return entry, nil
- })
- if entry, ok := result.(*cachedCyberSessionBlockRuntime); ok && entry != nil {
- return entry.enabled, entry.ttl
- }
- return false, time.Hour
- }
- // GetAntigravityUserAgentVersion 返回 Antigravity 上游请求使用的版本号。
- // 后台设置优先;为空、缺失或非法时回退到 ANTIGRAVITY_USER_AGENT_VERSION / 内置默认值。
- func (s *SettingService) GetAntigravityUserAgentVersion(ctx context.Context) string {
- fallback := antigravity.GetDefaultUserAgentVersion()
- if s == nil || s.settingRepo == nil {
- return fallback
- }
- if cached, ok := s.antigravityUAVersionCache.Load().(*cachedAntigravityUserAgentVersion); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.version
- }
- }
- result, _, _ := s.antigravityUAVersionSF.Do("antigravity_user_agent_version", func() (any, error) {
- if cached, ok := s.antigravityUAVersionCache.Load().(*cachedAntigravityUserAgentVersion); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.version, nil
- }
- }
- if ctx == nil {
- ctx = context.Background()
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), antigravityUserAgentVersionDBTimeout)
- defer cancel()
- value, err := s.settingRepo.GetValue(dbCtx, SettingKeyAntigravityUserAgentVersion)
- if err != nil && !errors.Is(err, ErrSettingNotFound) {
- slog.Warn("failed to get antigravity user agent version setting", "error", err)
- s.antigravityUAVersionCache.Store(&cachedAntigravityUserAgentVersion{
- version: fallback,
- expiresAt: time.Now().Add(antigravityUserAgentVersionErrorTTL).UnixNano(),
- })
- return fallback, nil
- }
- version := antigravity.NormalizeUserAgentVersion(value)
- if version == "" {
- version = fallback
- }
- s.antigravityUAVersionCache.Store(&cachedAntigravityUserAgentVersion{
- version: version,
- expiresAt: time.Now().Add(antigravityUserAgentVersionCacheTTL).UnixNano(),
- })
- return version, nil
- })
- if version, ok := result.(string); ok && version != "" {
- return version
- }
- return fallback
- }
- // GetOpenAICodexUserAgent 返回 OpenAI Codex 上游请求使用的 User-Agent。
- // 后台设置优先;为空时回退到内置默认值。
- func (s *SettingService) GetOpenAICodexUserAgent(ctx context.Context) string {
- fallback := DefaultOpenAICodexUserAgent
- if s == nil || s.settingRepo == nil {
- return fallback
- }
- if cached, ok := s.openAICodexUACache.Load().(*cachedOpenAICodexUserAgent); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.value
- }
- }
- result, _, _ := s.openAICodexUASF.Do("openai_codex_user_agent", func() (any, error) {
- if cached, ok := s.openAICodexUACache.Load().(*cachedOpenAICodexUserAgent); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.value, nil
- }
- }
- if ctx == nil {
- ctx = context.Background()
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), openAICodexUserAgentDBTimeout)
- defer cancel()
- value, err := s.settingRepo.GetValue(dbCtx, SettingKeyOpenAICodexUserAgent)
- if err != nil && !errors.Is(err, ErrSettingNotFound) {
- slog.Warn("failed to get openai codex user agent setting", "error", err)
- s.openAICodexUACache.Store(&cachedOpenAICodexUserAgent{
- value: fallback,
- expiresAt: time.Now().Add(openAICodexUserAgentErrorTTL).UnixNano(),
- })
- return fallback, nil
- }
- ua := strings.TrimSpace(value)
- if ua == "" {
- ua = fallback
- }
- s.openAICodexUACache.Store(&cachedOpenAICodexUserAgent{
- value: ua,
- expiresAt: time.Now().Add(openAICodexUserAgentCacheTTL).UnixNano(),
- })
- return ua, nil
- })
- if ua, ok := result.(string); ok && ua != "" {
- return ua
- }
- return fallback
- }
- // GetOpenAICodexClientVersion 返回出站声明的 Codex 客户端版本号。
- // 优先级:管理员在面板覆写的版本 → 自动同步到的官方最新稳定版 → 内置常量。
- // 上游在容量紧张时按客户端身份分优先级降载,陈旧版本会被优先丢弃,故该值需保持跟随官方发布;
- // 自动同步让运维不必为了跟版本而发新版本。
- func (s *SettingService) GetOpenAICodexClientVersion(ctx context.Context) string {
- fallback := codexCLIVersion
- if s == nil || s.settingRepo == nil {
- return fallback
- }
- if cached, ok := s.openAICodexVersionCache.Load().(*cachedOpenAICodexClientVersion); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.version
- }
- }
- result, _, _ := s.openAICodexVersionSF.Do(openAICodexClientVersionSFKey, func() (any, error) {
- if cached, ok := s.openAICodexVersionCache.Load().(*cachedOpenAICodexClientVersion); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.version, nil
- }
- }
- if ctx == nil {
- ctx = context.Background()
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), openAICodexClientVersionDBTimeout)
- defer cancel()
- values, err := s.settingRepo.GetMultiple(dbCtx, []string{
- SettingKeyOpenAICodexClientVersion,
- SettingKeyOpenAICodexClientVersionSynced,
- })
- if err != nil {
- slog.Warn("failed to get openai codex client version setting", "error", err)
- s.openAICodexVersionCache.Store(&cachedOpenAICodexClientVersion{
- version: fallback,
- expiresAt: time.Now().Add(openAICodexClientVersionErrorTTL).UnixNano(),
- })
- return fallback, nil
- }
- version := NormalizeCodexClientVersion(values[SettingKeyOpenAICodexClientVersion])
- if version == "" {
- version = NormalizeCodexClientVersion(values[SettingKeyOpenAICodexClientVersionSynced])
- }
- if version == "" {
- version = fallback
- }
- s.openAICodexVersionCache.Store(&cachedOpenAICodexClientVersion{
- version: version,
- expiresAt: time.Now().Add(openAICodexClientVersionCacheTTL).UnixNano(),
- })
- return version, nil
- })
- if version, ok := result.(string); ok && version != "" {
- return version
- }
- return fallback
- }
- // InvalidateOpenAICodexClientVersionCache 丢弃版本号缓存,下次读取回源。
- // 面板保存与自动同步写入后调用。
- func (s *SettingService) InvalidateOpenAICodexClientVersionCache() {
- if s == nil {
- return
- }
- s.openAICodexVersionSF.Forget(openAICodexClientVersionSFKey)
- s.openAICodexVersionCache.Store((*cachedOpenAICodexClientVersion)(nil))
- }
- // GetOpenAICodexCanonicalUserAgent 返回出站规范 Codex User-Agent。
- // 未填面板 UA 时按当前生效的客户端版本号拼出标准 Codex TUI UA。
- //
- // 面板 UA 只贡献客户端名与 OS / 架构 / 终端指纹,版本段一律用生效版本重建:该输入框是
- // 唯一能改 UA 后缀的地方,但它填写于某个历史版本,逐字沿用会把出站身份永久钉死在陈旧
- // 版本上并绕过自动同步——而陈旧身份正是上游优先降载的那一侧。
- // 需要固定版本请填「Codex 客户端版本号」并关闭自动同步。
- func (s *SettingService) GetOpenAICodexCanonicalUserAgent(ctx context.Context) string {
- if s == nil {
- return codexCLIUserAgent
- }
- version := s.GetOpenAICodexClientVersion(ctx)
- ua := strings.TrimSpace(s.GetOpenAICodexUserAgent(ctx))
- if ua == "" {
- return buildCodexCLIUserAgent(version)
- }
- if rebuilt := openai.SetCodexUserAgentVersion(ua, version); rebuilt != "" {
- return rebuilt
- }
- // 非 `{client}/{version}` 形态:交给 PairCodexClientIdentity 判定,
- // 推导不出官方身份时由收口整体回退规范身份。
- return ua
- }
- var legacyClaudeCodeCodexWhitelistEntry = openai.AllowedClientEntry{
- Originator: "Claude Code",
- UAContains: []string{"Claude Code/"},
- }
- // MigrateOpenAIAllowClaudeCodeCodexPluginSetting folds the deprecated global Claude Code
- // plugin allow switch into codex_cli_only_whitelist. The app-server identity model is the
- // same originator + UA marker pair, so runtime checks no longer need a separate flag.
- func (s *SettingService) MigrateOpenAIAllowClaudeCodeCodexPluginSetting(ctx context.Context) error {
- if s == nil || s.settingRepo == nil {
- return nil
- }
- if ctx == nil {
- ctx = context.Background()
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), codexRestrictionPolicyDBTimeout)
- defer cancel()
- legacyValue, err := s.settingRepo.GetValue(dbCtx, SettingKeyOpenAIAllowClaudeCodeCodexPlugin)
- if err != nil {
- if errors.Is(err, ErrSettingNotFound) {
- return nil
- }
- return fmt.Errorf("get deprecated %s setting: %w", SettingKeyOpenAIAllowClaudeCodeCodexPlugin, err)
- }
- if strings.TrimSpace(legacyValue) != "true" {
- return nil
- }
- rawWhitelist, err := s.settingRepo.GetValue(dbCtx, SettingKeyCodexCLIOnlyWhitelist)
- if err != nil && !errors.Is(err, ErrSettingNotFound) {
- return fmt.Errorf("get %s setting: %w", SettingKeyCodexCLIOnlyWhitelist, err)
- }
- var entries []openai.AllowedClientEntry
- if strings.TrimSpace(rawWhitelist) != "" {
- if err := json.Unmarshal([]byte(rawWhitelist), &entries); err != nil {
- return fmt.Errorf("parse %s setting: %w", SettingKeyCodexCLIOnlyWhitelist, err)
- }
- }
- if codexClientEntriesContain(entries, legacyClaudeCodeCodexWhitelistEntry) {
- return nil
- }
- entries = append(entries, legacyClaudeCodeCodexWhitelistEntry)
- encoded, err := json.Marshal(entries)
- if err != nil {
- return fmt.Errorf("marshal %s setting: %w", SettingKeyCodexCLIOnlyWhitelist, err)
- }
- if err := s.settingRepo.Set(dbCtx, SettingKeyCodexCLIOnlyWhitelist, string(encoded)); err != nil {
- return fmt.Errorf("set %s setting: %w", SettingKeyCodexCLIOnlyWhitelist, err)
- }
- s.codexRestrictionPolicySF.Forget("codex_restriction_policy")
- s.codexRestrictionPolicyCache.Store(&cachedCodexRestrictionPolicy{expiresAt: 0})
- return nil
- }
- // MigrateGrokDefaultTextModel upgrades the pre-4.6 built-in default for
- // existing installations. Explicit operator choices are left untouched.
- func (s *SettingService) MigrateGrokDefaultTextModel(ctx context.Context) error {
- if s == nil || s.settingRepo == nil {
- return nil
- }
- if ctx == nil {
- ctx = context.Background()
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), codexRestrictionPolicyDBTimeout)
- defer cancel()
- value, err := s.settingRepo.GetValue(dbCtx, SettingKeyGrokDefaultTextModel)
- if err != nil {
- if errors.Is(err, ErrSettingNotFound) {
- return nil
- }
- return fmt.Errorf("get %s setting: %w", SettingKeyGrokDefaultTextModel, err)
- }
- // Only migrate the value that was previously shipped as the built-in
- // default. Any other value is an explicit operator choice or a future
- // default and must remain unchanged.
- if strings.TrimSpace(value) != "grok-4.5" {
- return nil
- }
- if err := s.settingRepo.Set(dbCtx, SettingKeyGrokDefaultTextModel, "grok-4.6"); err != nil {
- return fmt.Errorf("set %s setting: %w", SettingKeyGrokDefaultTextModel, err)
- }
- return nil
- }
- // MigrateCodexBodyFingerprintToSignals 把已废弃的 codex_cli_only_allow_body_engine_fingerprint
- // 开关并入引擎指纹信号列表。幂等:信号键已存在(非空)则不动;缺失时写默认种子,
- // 并把 body 路径行的 Required 设为旧 body 开关的值(旧 true ⇒ 勾上 body 行)。
- func (s *SettingService) MigrateCodexBodyFingerprintToSignals(ctx context.Context) error {
- if s == nil || s.settingRepo == nil {
- return nil
- }
- if ctx == nil {
- ctx = context.Background()
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), codexRestrictionPolicyDBTimeout)
- defer cancel()
- if v, err := s.settingRepo.GetValue(dbCtx, SettingKeyCodexCLIOnlyEngineFingerprintSignals); err == nil && strings.TrimSpace(v) != "" {
- return nil // 已配置/已迁移
- } else if err != nil && !errors.Is(err, ErrSettingNotFound) {
- return fmt.Errorf("get %s setting: %w", SettingKeyCodexCLIOnlyEngineFingerprintSignals, err)
- }
- bodyOn := false
- if v, err := s.settingRepo.GetValue(dbCtx, SettingKeyCodexCLIOnlyAllowBodyEngineFingerprint); err == nil {
- bodyOn = strings.TrimSpace(v) == "true"
- } else if !errors.Is(err, ErrSettingNotFound) {
- return fmt.Errorf("get deprecated %s setting: %w", SettingKeyCodexCLIOnlyAllowBodyEngineFingerprint, err)
- }
- seed := make([]openai.EngineFingerprintSignal, len(openai.DefaultEngineFingerprintSignals))
- copy(seed, openai.DefaultEngineFingerprintSignals)
- if bodyOn {
- for i := range seed {
- if seed[i].Type == openai.FingerprintSignalBodyPath {
- seed[i].Required = true
- }
- }
- }
- encoded, err := json.Marshal(seed)
- if err != nil {
- return fmt.Errorf("marshal %s setting: %w", SettingKeyCodexCLIOnlyEngineFingerprintSignals, err)
- }
- if err := s.settingRepo.Set(dbCtx, SettingKeyCodexCLIOnlyEngineFingerprintSignals, string(encoded)); err != nil {
- return fmt.Errorf("set %s setting: %w", SettingKeyCodexCLIOnlyEngineFingerprintSignals, err)
- }
- s.codexRestrictionPolicySF.Forget("codex_restriction_policy")
- s.codexRestrictionPolicyCache.Store(&cachedCodexRestrictionPolicy{expiresAt: 0})
- return nil
- }
- func codexClientEntriesContain(entries []openai.AllowedClientEntry, want openai.AllowedClientEntry) bool {
- wantOriginator := strings.TrimSpace(want.Originator)
- if wantOriginator == "" {
- return false
- }
- wantMarkers := normalizedCodexClientMarkers(want.UAContains)
- if len(wantMarkers) == 0 {
- return false
- }
- for _, entry := range entries {
- if !strings.EqualFold(strings.TrimSpace(entry.Originator), wantOriginator) {
- continue
- }
- gotMarkers := normalizedCodexClientMarkers(entry.UAContains)
- if len(gotMarkers) != len(wantMarkers) {
- continue
- }
- matched := true
- for marker := range wantMarkers {
- if _, ok := gotMarkers[marker]; !ok {
- matched = false
- break
- }
- }
- if matched {
- return true
- }
- }
- return false
- }
- func normalizedCodexClientMarkers(markers []string) map[string]struct{} {
- normalized := make(map[string]struct{}, len(markers))
- for _, marker := range markers {
- marker = strings.TrimSpace(marker)
- if marker == "" {
- continue
- }
- normalized[strings.ToLower(marker)] = struct{}{}
- }
- return normalized
- }
- // GetCodexRestrictionPolicy 读取 codex_cli_only 全局加固策略(黑/白名单、最低版本、引擎指纹门)。
- // 仅在调用方已确认账号 codex_cli_only 开启时读取;进程内 atomic.Value 缓存(60s TTL)避免热路径访问 DB。
- // 任意键缺失/解析失败 → 安全默认:空名单、空版本、默认种子指纹信号。
- func (s *SettingService) GetCodexRestrictionPolicy(ctx context.Context) CodexRestrictionPolicy {
- if cached, ok := s.codexRestrictionPolicyCache.Load().(*cachedCodexRestrictionPolicy); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.value
- }
- }
- result, _, _ := s.codexRestrictionPolicySF.Do("codex_restriction_policy", func() (any, error) {
- if cached, ok := s.codexRestrictionPolicyCache.Load().(*cachedCodexRestrictionPolicy); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.value, nil
- }
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), codexRestrictionPolicyDBTimeout)
- defer cancel()
- pol := CodexRestrictionPolicy{EngineFingerprintSignals: openai.DefaultEngineFingerprintSignals} // 安全默认:默认种子指纹信号
- if v, err := s.settingRepo.GetValue(dbCtx, SettingKeyMinCodexVersion); err == nil {
- pol.MinCodexVersion = strings.TrimSpace(v)
- }
- if v, err := s.settingRepo.GetValue(dbCtx, SettingKeyMaxCodexVersion); err == nil {
- pol.MaxCodexVersion = strings.TrimSpace(v)
- }
- if v, err := s.settingRepo.GetValue(dbCtx, SettingKeyCodexCLIOnlyAllowAppServerClients); err == nil {
- pol.AllowAppServerClients = strings.TrimSpace(v) == "true" // 仅显式 "true" 开启
- }
- pol.EngineFingerprintSignals = s.loadEngineFingerprintSignals(dbCtx)
- pol.Whitelist = s.loadCodexClientEntries(dbCtx, SettingKeyCodexCLIOnlyWhitelist)
- pol.Blacklist = s.loadCodexClientEntries(dbCtx, SettingKeyCodexCLIOnlyBlacklist)
- s.codexRestrictionPolicyCache.Store(&cachedCodexRestrictionPolicy{
- value: pol,
- expiresAt: time.Now().Add(codexRestrictionPolicyCacheTTL).UnixNano(),
- })
- return pol, nil
- })
- if pol, ok := result.(CodexRestrictionPolicy); ok {
- return pol
- }
- return CodexRestrictionPolicy{EngineFingerprintSignals: openai.DefaultEngineFingerprintSignals}
- }
- // loadCodexClientEntries 读取并解析 []openai.AllowedClientEntry JSON 设置;缺失/空/非法 → nil(安全忽略)。
- func (s *SettingService) loadCodexClientEntries(ctx context.Context, key string) []openai.AllowedClientEntry {
- v, err := s.settingRepo.GetValue(ctx, key)
- if err != nil || strings.TrimSpace(v) == "" {
- return nil
- }
- var entries []openai.AllowedClientEntry
- if json.Unmarshal([]byte(v), &entries) != nil {
- return nil
- }
- return entries
- }
- // loadEngineFingerprintSignals 读取引擎指纹信号列表;缺失/空/非法 → 默认种子。
- func (s *SettingService) loadEngineFingerprintSignals(ctx context.Context) []openai.EngineFingerprintSignal {
- v, err := s.settingRepo.GetValue(ctx, SettingKeyCodexCLIOnlyEngineFingerprintSignals)
- if err != nil || strings.TrimSpace(v) == "" {
- return openai.DefaultEngineFingerprintSignals
- }
- sigs, ok := openai.ParseEngineFingerprintSignals(v)
- if !ok {
- return openai.DefaultEngineFingerprintSignals
- }
- return sigs
- }
- // ValidateCodexClientEntriesJSON 校验 codex_cli_only 名单 JSON 配置(黑名单语义):
- // 空=合法(禁用);非空须为 []AllowedClientEntry 的 JSON 数组。黑名单是 OR 宽 deny,
- // 允许 originator-only 条目,故不校验 ua_contains。白名单请用 ValidateCodexWhitelistEntriesJSON。
- func ValidateCodexClientEntriesJSON(raw string) error {
- trimmed := strings.TrimSpace(raw)
- if trimmed == "" {
- return nil
- }
- var entries []openai.AllowedClientEntry
- if err := json.Unmarshal([]byte(trimmed), &entries); err != nil {
- return fmt.Errorf("must be empty or a valid JSON array of {originator, ua_contains}")
- }
- return nil
- }
- // ValidateCodexWhitelistEntriesJSON 在 ValidateCodexClientEntriesJSON 的数组结构校验之上,额外要求
- // 每条白名单条目「有可能命中」(openai.AllowedClientEntry.IsWhitelistable)。白名单是双因子 AND:
- // originator-only、空或含空白 ua_contains 的条目会在运行时静默失效——这里让管理员在写入时即收到反馈,
- // 而非存入永不命中的死规则。黑名单(OR 宽 deny)仍用 ValidateCodexClientEntriesJSON。
- func ValidateCodexWhitelistEntriesJSON(raw string) error {
- trimmed := strings.TrimSpace(raw)
- if trimmed == "" {
- return nil
- }
- var entries []openai.AllowedClientEntry
- if err := json.Unmarshal([]byte(trimmed), &entries); err != nil {
- return fmt.Errorf("must be empty or a valid JSON array of {originator, ua_contains}")
- }
- for i, e := range entries {
- if !e.IsWhitelistable() {
- return fmt.Errorf("entry %d: whitelist requires a non-empty originator and at least one non-empty ua_contains (double-factor AND; otherwise the rule never matches)", i)
- }
- }
- return nil
- }
- // ValidateEngineFingerprintSignalsJSON 服务层包装,复用 openai 校验逻辑。
- func ValidateEngineFingerprintSignalsJSON(raw string) error {
- return openai.ValidateEngineFingerprintSignalsJSON(raw)
- }
- // IsBackendModeEnabled checks if backend mode is enabled
- // Uses in-process atomic.Value cache with 60s TTL, zero-lock hot path
- func (s *SettingService) IsBackendModeEnabled(ctx context.Context) bool {
- if cached, ok := backendModeCache.Load().(*cachedBackendMode); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.value
- }
- }
- result, _, _ := backendModeSF.Do("backend_mode", func() (any, error) {
- if cached, ok := backendModeCache.Load().(*cachedBackendMode); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.value, nil
- }
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), backendModeDBTimeout)
- defer cancel()
- value, err := s.settingRepo.GetValue(dbCtx, SettingKeyBackendModeEnabled)
- if err != nil {
- if errors.Is(err, ErrSettingNotFound) {
- // Setting not yet created (fresh install) - default to disabled with full TTL
- backendModeCache.Store(&cachedBackendMode{
- value: false,
- expiresAt: time.Now().Add(backendModeCacheTTL).UnixNano(),
- })
- return false, nil
- }
- slog.Warn("failed to get backend_mode_enabled setting", "error", err)
- backendModeCache.Store(&cachedBackendMode{
- value: false,
- expiresAt: time.Now().Add(backendModeErrorTTL).UnixNano(),
- })
- return false, nil
- }
- enabled := value == "true"
- backendModeCache.Store(&cachedBackendMode{
- value: enabled,
- expiresAt: time.Now().Add(backendModeCacheTTL).UnixNano(),
- })
- return enabled, nil
- })
- if val, ok := result.(bool); ok {
- return val
- }
- return false
- }
- type gatewayForwardingSettingsResult struct {
- openAITTFTMode string
- fp, mp, cch, claudeOAuthSystemPromptInjection, cacheTTL1h, rewriteMessageCacheControl bool
- clientDatelineNormalization bool
- claudeOAuthSystemPrompt, claudeOAuthSystemPromptBlocks string
- }
- func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context) gatewayForwardingSettingsResult {
- if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return gatewayForwardingSettingsResult{
- openAITTFTMode: cached.openAITTFTMode,
- fp: cached.fingerprintUnification,
- mp: cached.metadataPassthrough,
- cch: cached.cchSigning,
- claudeOAuthSystemPromptInjection: cached.claudeOAuthSystemPromptInjection,
- claudeOAuthSystemPrompt: cached.claudeOAuthSystemPrompt,
- claudeOAuthSystemPromptBlocks: cached.claudeOAuthSystemPromptBlocks,
- cacheTTL1h: cached.anthropicCacheTTL1hInjection,
- rewriteMessageCacheControl: cached.rewriteMessageCacheControl,
- clientDatelineNormalization: cached.clientDatelineNormalization,
- }
- }
- }
- val, _, _ := gatewayForwardingSF.Do("gateway_forwarding", func() (any, error) {
- if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil {
- if time.Now().UnixNano() < cached.expiresAt {
- return gatewayForwardingSettingsResult{
- openAITTFTMode: cached.openAITTFTMode,
- fp: cached.fingerprintUnification,
- mp: cached.metadataPassthrough,
- cch: cached.cchSigning,
- claudeOAuthSystemPromptInjection: cached.claudeOAuthSystemPromptInjection,
- claudeOAuthSystemPrompt: cached.claudeOAuthSystemPrompt,
- claudeOAuthSystemPromptBlocks: cached.claudeOAuthSystemPromptBlocks,
- cacheTTL1h: cached.anthropicCacheTTL1hInjection,
- rewriteMessageCacheControl: cached.rewriteMessageCacheControl,
- clientDatelineNormalization: cached.clientDatelineNormalization,
- }, nil
- }
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout)
- defer cancel()
- values, err := s.settingRepo.GetMultiple(dbCtx, []string{
- SettingKeyOpenAITTFTMode,
- SettingKeyEnableFingerprintUnification,
- SettingKeyEnableMetadataPassthrough,
- SettingKeyEnableCCHSigning,
- SettingKeyEnableClaudeOAuthSystemPromptInjection,
- SettingKeyClaudeOAuthSystemPrompt,
- SettingKeyClaudeOAuthSystemPromptBlocks,
- SettingKeyEnableAnthropicCacheTTL1hInjection,
- SettingKeyRewriteMessageCacheControl,
- SettingKeyEnableClientDatelineNormalization,
- })
- if err != nil {
- slog.Warn("failed to get gateway forwarding settings", "error", err)
- gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
- openAITTFTMode: OpenAITTFTModeSemantic,
- fingerprintUnification: true,
- metadataPassthrough: false,
- cchSigning: false,
- claudeOAuthSystemPromptInjection: true,
- anthropicCacheTTL1hInjection: false,
- rewriteMessageCacheControl: s.defaultRewriteMessageCacheControl(),
- clientDatelineNormalization: true,
- expiresAt: time.Now().Add(gatewayForwardingErrorTTL).UnixNano(),
- })
- return gatewayForwardingSettingsResult{openAITTFTMode: OpenAITTFTModeSemantic, fp: true, claudeOAuthSystemPromptInjection: true, rewriteMessageCacheControl: s.defaultRewriteMessageCacheControl(), clientDatelineNormalization: true}, nil
- }
- ttftMode := normalizeOpenAITTFTMode(values[SettingKeyOpenAITTFTMode])
- fp := true
- if v, ok := values[SettingKeyEnableFingerprintUnification]; ok && v != "" {
- fp = v == "true"
- }
- mp := values[SettingKeyEnableMetadataPassthrough] == "true"
- cch := values[SettingKeyEnableCCHSigning] == "true"
- systemPromptInjection := true
- if v, ok := values[SettingKeyEnableClaudeOAuthSystemPromptInjection]; ok && v != "" {
- systemPromptInjection = v == "true"
- }
- systemPrompt := values[SettingKeyClaudeOAuthSystemPrompt]
- systemPromptBlocks := values[SettingKeyClaudeOAuthSystemPromptBlocks]
- cacheTTL1h := values[SettingKeyEnableAnthropicCacheTTL1hInjection] == "true"
- rewriteMessageCacheControl := s.defaultRewriteMessageCacheControl()
- if v, ok := values[SettingKeyRewriteMessageCacheControl]; ok && v != "" {
- rewriteMessageCacheControl = v == "true"
- }
- clientDatelineNormalization := true
- if v, ok := values[SettingKeyEnableClientDatelineNormalization]; ok && v != "" {
- clientDatelineNormalization = v == "true"
- }
- gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
- openAITTFTMode: ttftMode,
- fingerprintUnification: fp,
- metadataPassthrough: mp,
- cchSigning: cch,
- claudeOAuthSystemPromptInjection: systemPromptInjection,
- claudeOAuthSystemPrompt: systemPrompt,
- claudeOAuthSystemPromptBlocks: systemPromptBlocks,
- anthropicCacheTTL1hInjection: cacheTTL1h,
- rewriteMessageCacheControl: rewriteMessageCacheControl,
- clientDatelineNormalization: clientDatelineNormalization,
- expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
- })
- return gatewayForwardingSettingsResult{
- openAITTFTMode: ttftMode,
- fp: fp,
- mp: mp,
- cch: cch,
- claudeOAuthSystemPromptInjection: systemPromptInjection,
- claudeOAuthSystemPrompt: systemPrompt,
- claudeOAuthSystemPromptBlocks: systemPromptBlocks,
- cacheTTL1h: cacheTTL1h,
- rewriteMessageCacheControl: rewriteMessageCacheControl,
- clientDatelineNormalization: clientDatelineNormalization,
- }, nil
- })
- if r, ok := val.(gatewayForwardingSettingsResult); ok {
- return r
- }
- return gatewayForwardingSettingsResult{fp: true, claudeOAuthSystemPromptInjection: true, clientDatelineNormalization: true}
- }
- // GetOpenAITTFTMode 返回 Responses first_token_ms 的统计口径。
- func (s *SettingService) GetOpenAITTFTMode(ctx context.Context) string {
- return s.getGatewayForwardingSettingsCached(ctx).openAITTFTMode
- }
- // GetGatewayForwardingSettings returns cached gateway forwarding settings.
- // Uses in-process atomic.Value cache with 60s TTL, zero-lock hot path.
- // Returns (fingerprintUnification, metadataPassthrough, cchSigning).
- func (s *SettingService) GetGatewayForwardingSettings(ctx context.Context) (fingerprintUnification, metadataPassthrough, cchSigning bool) {
- result := s.getGatewayForwardingSettingsCached(ctx)
- return result.fp, result.mp, result.cch
- }
- // IsAnthropicCacheTTL1hInjectionEnabled 检查是否对 Anthropic OAuth/SetupToken 请求体注入 1h cache_control ttl。
- func (s *SettingService) IsAnthropicCacheTTL1hInjectionEnabled(ctx context.Context) bool {
- return s.getGatewayForwardingSettingsCached(ctx).cacheTTL1h
- }
- // IsRewriteMessageCacheControlEnabled 检查是否启用 messages cache_control 改写。
- func (s *SettingService) IsRewriteMessageCacheControlEnabled(ctx context.Context) bool {
- return s.getGatewayForwardingSettingsCached(ctx).rewriteMessageCacheControl
- }
- // IsClientDatelineNormalizationEnabled 检查是否启用 Anthropic OAuth/SetupToken 请求体
- // 的客户端 dateline 归一化。默认开启。
- func (s *SettingService) IsClientDatelineNormalizationEnabled(ctx context.Context) bool {
- return s.getGatewayForwardingSettingsCached(ctx).clientDatelineNormalization
- }
- // GetClaudeOAuthSystemPromptInjectionSettings returns the Claude OAuth mimic
- // system block switch, legacy custom expansion prompt, and configurable blocks JSON.
- // Empty values mean use the built-in Claude Code default blocks.
- func (s *SettingService) GetClaudeOAuthSystemPromptInjectionSettings(ctx context.Context) (enabled bool, prompt string, blocks string) {
- result := s.getGatewayForwardingSettingsCached(ctx)
- return result.claudeOAuthSystemPromptInjection, result.claudeOAuthSystemPrompt, result.claudeOAuthSystemPromptBlocks
- }
- // GetClaudeCodeVersionBounds 获取 Claude Code 版本号上下限要求
- // 使用进程内 atomic.Value 缓存,60 秒 TTL,热路径零锁开销
- // singleflight 防止缓存过期时 thundering herd
- // 返回空字符串表示不做对应方向的版本检查
- func (s *SettingService) GetClaudeCodeVersionBounds(ctx context.Context) (min, max string) {
- if cached, ok := versionBoundsCache.Load().(*cachedVersionBounds); ok {
- if time.Now().UnixNano() < cached.expiresAt {
- return cached.min, cached.max
- }
- }
- // singleflight: 同一时刻只有一个 goroutine 查询 DB,其余复用结果
- type bounds struct{ min, max string }
- result, err, _ := versionBoundsSF.Do("version_bounds", func() (any, error) {
- // 二次检查,避免排队的 goroutine 重复查询
- if cached, ok := versionBoundsCache.Load().(*cachedVersionBounds); ok {
- if time.Now().UnixNano() < cached.expiresAt {
- return bounds{cached.min, cached.max}, nil
- }
- }
- // 使用独立 context:断开请求取消链,避免客户端断连导致空值被长期缓存
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), versionBoundsDBTimeout)
- defer cancel()
- values, err := s.settingRepo.GetMultiple(dbCtx, []string{
- SettingKeyMinClaudeCodeVersion,
- SettingKeyMaxClaudeCodeVersion,
- })
- if err != nil {
- // fail-open: DB 错误时不阻塞请求,但记录日志并使用短 TTL 快速重试
- slog.Warn("failed to get claude code version bounds setting, skipping version check", "error", err)
- versionBoundsCache.Store(&cachedVersionBounds{
- min: "",
- max: "",
- expiresAt: time.Now().Add(versionBoundsErrorTTL).UnixNano(),
- })
- return bounds{"", ""}, nil
- }
- b := bounds{
- min: values[SettingKeyMinClaudeCodeVersion],
- max: values[SettingKeyMaxClaudeCodeVersion],
- }
- versionBoundsCache.Store(&cachedVersionBounds{
- min: b.min,
- max: b.max,
- expiresAt: time.Now().Add(versionBoundsCacheTTL).UnixNano(),
- })
- return b, nil
- })
- if err != nil {
- return "", ""
- }
- b, ok := result.(bounds)
- if !ok {
- return "", ""
- }
- return b.min, b.max
- }
- // GetOpenAIQuotaAutoPauseSettings returns the current global default quota auto-pause
- // settings. It is invoked on the OpenAI scheduling hot path (once per request) and is
- // therefore designed to never block on the DB:
- //
- // - Fresh cached value → returned immediately.
- // - Stale or empty cache → the last known value is returned, and a background
- // goroutine refreshes the cache via singleflight (stale-while-revalidate).
- // - First call with no cache yet → zero defaults are returned and the same async
- // refresh is kicked off; the next call gets the freshly populated value.
- //
- // Callers that need the freshly persisted value synchronously (tests, post-update
- // confirmation, optional startup warm-up) should call WarmOpenAIQuotaAutoPauseSettings.
- func (s *SettingService) GetOpenAIQuotaAutoPauseSettings(ctx context.Context) OpsOpenAIAccountQuotaAutoPauseSettings {
- if s == nil {
- return OpsOpenAIAccountQuotaAutoPauseSettings{}
- }
- cached, _ := s.openAIQuotaAutoPauseSettingsCache.Load().(*cachedOpenAIQuotaAutoPauseSettings)
- now := time.Now().UnixNano()
- if cached != nil && now < cached.expiresAt {
- return cached.settings
- }
- // Stale or unset: trigger background refresh without blocking this request.
- // singleflight.DoChan dedupes concurrent refreshes; we deliberately ignore the
- // returned channel — the result is observable via the atomic cache.
- s.openAIQuotaAutoPauseSettingsSF.DoChan(openAIQuotaAutoPauseSettingsRefreshKey, func() (any, error) {
- s.refreshOpenAIQuotaAutoPauseSettings(context.Background())
- return nil, nil
- })
- if cached != nil {
- return cached.settings // serve stale value while revalidating
- }
- return OpsOpenAIAccountQuotaAutoPauseSettings{}
- }
- // WarmOpenAIQuotaAutoPauseSettings synchronously loads the quota auto-pause settings
- // into the in-memory cache. Useful for application startup (so the first request hits
- // a warm cache) and for tests that need deterministic reads immediately after
- // constructing the service.
- func (s *SettingService) WarmOpenAIQuotaAutoPauseSettings(ctx context.Context) OpsOpenAIAccountQuotaAutoPauseSettings {
- if s == nil {
- return OpsOpenAIAccountQuotaAutoPauseSettings{}
- }
- s.refreshOpenAIQuotaAutoPauseSettings(ctx)
- cached, _ := s.openAIQuotaAutoPauseSettingsCache.Load().(*cachedOpenAIQuotaAutoPauseSettings)
- if cached == nil {
- return OpsOpenAIAccountQuotaAutoPauseSettings{}
- }
- return cached.settings
- }
- // refreshOpenAIQuotaAutoPauseSettings reads the latest settings from the DB and stores
- // them into the in-memory cache. On error it stores the prior value (or zero defaults
- // if nothing is cached yet) with the shorter error TTL so the next refresh comes
- // sooner. Always uses its own timeout-bounded context to keep refresh latency
- // predictable regardless of the caller.
- func (s *SettingService) refreshOpenAIQuotaAutoPauseSettings(ctx context.Context) {
- if s == nil || s.settingRepo == nil {
- return
- }
- dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), openAIQuotaAutoPauseSettingsDBTimeout)
- defer cancel()
- settings := OpsOpenAIAccountQuotaAutoPauseSettings{}
- ttl := openAIQuotaAutoPauseSettingsCacheTTL
- raw, err := s.settingRepo.GetValue(dbCtx, SettingKeyOpsAdvancedSettings)
- if err == nil {
- cfg := defaultOpsAdvancedSettings()
- if strings.TrimSpace(raw) != "" {
- if jsonErr := json.Unmarshal([]byte(raw), cfg); jsonErr == nil {
- normalizeOpsAdvancedSettings(cfg)
- }
- }
- settings = cfg.OpenAIAccountQuotaAutoPause
- } else if !errors.Is(err, ErrSettingNotFound) {
- // Real error: keep serving prior value but refresh sooner.
- if prior, _ := s.openAIQuotaAutoPauseSettingsCache.Load().(*cachedOpenAIQuotaAutoPauseSettings); prior != nil {
- settings = prior.settings
- }
- ttl = openAIQuotaAutoPauseSettingsErrorTTL
- }
- s.openAIQuotaAutoPauseSettingsCache.Store(&cachedOpenAIQuotaAutoPauseSettings{
- settings: settings,
- expiresAt: time.Now().Add(ttl).UnixNano(),
- })
- }
- // SetOpenAIQuotaAutoPauseSettings writes the given settings directly into the in-memory
- // cache. Called from settings-write code paths so that the next read reflects the new
- // value immediately, without waiting for the background refresh.
- func (s *SettingService) SetOpenAIQuotaAutoPauseSettings(settings OpsOpenAIAccountQuotaAutoPauseSettings) {
- if s == nil {
- return
- }
- s.openAIQuotaAutoPauseSettingsCache.Store(&cachedOpenAIQuotaAutoPauseSettings{
- settings: settings,
- expiresAt: time.Now().Add(openAIQuotaAutoPauseSettingsCacheTTL).UnixNano(),
- })
- }
|