| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863 |
- // Package config provides configuration loading, defaults, and validation.
- package config
- import (
- "crypto/rand"
- "encoding/hex"
- "fmt"
- "log/slog"
- "math"
- "net/textproto"
- "net/url"
- "os"
- "strings"
- "sync/atomic"
- "time"
- "github.com/spf13/viper"
- "golang.org/x/net/http/httpguts"
- )
- const (
- RunModeStandard = "standard"
- RunModeSimple = "simple"
- )
- // 使用量记录队列溢出策略
- const (
- UsageRecordOverflowPolicyDrop = "drop"
- UsageRecordOverflowPolicySample = "sample"
- UsageRecordOverflowPolicySync = "sync"
- )
- // DefaultCSPPolicy is the default Content-Security-Policy with nonce support
- // __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
- const DefaultCSPPolicy = "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src 'self' https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
- // UMQ(用户消息队列)模式常量
- const (
- // UMQModeSerialize: 账号级串行锁 + RPM 自适应延迟
- UMQModeSerialize = "serialize"
- // UMQModeThrottle: 仅 RPM 自适应前置延迟,不阻塞并发
- UMQModeThrottle = "throttle"
- )
- // 连接池隔离策略常量
- // 用于控制上游 HTTP 连接池的隔离粒度,影响连接复用和资源消耗
- const (
- // ConnectionPoolIsolationProxy: 按代理隔离
- // 同一代理地址共享连接池,适合代理数量少、账户数量多的场景
- ConnectionPoolIsolationProxy = "proxy"
- // ConnectionPoolIsolationAccount: 按账户隔离
- // 每个账户独立连接池,适合账户数量少、需要严格隔离的场景
- ConnectionPoolIsolationAccount = "account"
- // ConnectionPoolIsolationAccountProxy: 按账户+代理组合隔离(默认)
- // 同一账户+代理组合共享连接池,提供最细粒度的隔离
- ConnectionPoolIsolationAccountProxy = "account_proxy"
- )
- // DefaultUpstreamResponseReadMaxBytes 上游非流式响应体的默认读取上限。
- // 128 MB 可容纳 2-3 张 4K PNG(base64 膨胀 33%,单张 4K PNG 最坏约 67MB base64)。
- // 可通过 gateway.upstream_response_read_max_bytes 配置项覆盖。
- const DefaultUpstreamResponseReadMaxBytes int64 = 128 * 1024 * 1024
- // DefaultModelsListReadMaxBytes 上游模型列表响应体的默认读取上限。
- // 可通过 gateway.models_list_read_max_bytes 配置项覆盖。
- const DefaultModelsListReadMaxBytes int64 = 8 * 1024 * 1024
- type Config struct {
- Server ServerConfig `mapstructure:"server"`
- Log LogConfig `mapstructure:"log"`
- CORS CORSConfig `mapstructure:"cors"`
- Security SecurityConfig `mapstructure:"security"`
- Billing BillingConfig `mapstructure:"billing"`
- Turnstile TurnstileConfig `mapstructure:"turnstile"`
- Database DatabaseConfig `mapstructure:"database"`
- Redis RedisConfig `mapstructure:"redis"`
- Ops OpsConfig `mapstructure:"ops"`
- JWT JWTConfig `mapstructure:"jwt"`
- Totp TotpConfig `mapstructure:"totp"`
- WebAuthn WebAuthnConfig `mapstructure:"webauthn"`
- LinuxDo LinuxDoConnectConfig `mapstructure:"linuxdo_connect"`
- WeChat WeChatConnectConfig `mapstructure:"wechat_connect"`
- OIDC OIDCConnectConfig `mapstructure:"oidc_connect"`
- DingTalk DingTalkConnectConfig `mapstructure:"dingtalk_connect"`
- GitHubOAuth EmailOAuthProviderConfig `mapstructure:"github_oauth"`
- GoogleOAuth EmailOAuthProviderConfig `mapstructure:"google_oauth"`
- Default DefaultConfig `mapstructure:"default"`
- RateLimit RateLimitConfig `mapstructure:"rate_limit"`
- Pricing PricingConfig `mapstructure:"pricing"`
- Gateway GatewayConfig `mapstructure:"gateway"`
- APIKeyAuth APIKeyAuthCacheConfig `mapstructure:"api_key_auth_cache"`
- SubscriptionCache SubscriptionCacheConfig `mapstructure:"subscription_cache"`
- SubscriptionMaintenance SubscriptionMaintenanceConfig `mapstructure:"subscription_maintenance"`
- Dashboard DashboardCacheConfig `mapstructure:"dashboard_cache"`
- DashboardAgg DashboardAggregationConfig `mapstructure:"dashboard_aggregation"`
- UsageCleanup UsageCleanupConfig `mapstructure:"usage_cleanup"`
- Concurrency ConcurrencyConfig `mapstructure:"concurrency"`
- TokenRefresh TokenRefreshConfig `mapstructure:"token_refresh"`
- RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
- Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
- Gemini GeminiConfig `mapstructure:"gemini"`
- Update UpdateConfig `mapstructure:"update"`
- Idempotency IdempotencyConfig `mapstructure:"idempotency"`
- BatchImage BatchImageConfig `mapstructure:"batch_image"`
- ImageStorage ImageStorageConfig `mapstructure:"image_storage"`
- Plugins PluginConfig `mapstructure:"plugins"`
- }
- // PluginConfig 控制管理员手动上传的本地进程插件。
- // 默认不包含插件,也不允许安装未签名插件;TrustedPublishers 用于追加第三方发布者。
- type PluginConfig struct {
- DataDir string `mapstructure:"data_dir"`
- AllowUnsigned bool `mapstructure:"allow_unsigned"`
- TrustedPublishers map[string]string `mapstructure:"trusted_publishers"`
- MaxUploadBytes int64 `mapstructure:"max_upload_bytes"`
- MaxUncompressedBytes int64 `mapstructure:"max_uncompressed_bytes"`
- StartTimeoutSeconds int `mapstructure:"start_timeout_seconds"`
- }
- type LogConfig struct {
- Level string `mapstructure:"level"`
- Format string `mapstructure:"format"`
- ServiceName string `mapstructure:"service_name"`
- Environment string `mapstructure:"env"`
- Caller bool `mapstructure:"caller"`
- StacktraceLevel string `mapstructure:"stacktrace_level"`
- Output LogOutputConfig `mapstructure:"output"`
- Rotation LogRotationConfig `mapstructure:"rotation"`
- Sampling LogSamplingConfig `mapstructure:"sampling"`
- }
- type LogOutputConfig struct {
- ToStdout bool `mapstructure:"to_stdout"`
- ToFile bool `mapstructure:"to_file"`
- FilePath string `mapstructure:"file_path"`
- }
- type LogRotationConfig struct {
- MaxSizeMB int `mapstructure:"max_size_mb"`
- MaxBackups int `mapstructure:"max_backups"`
- MaxAgeDays int `mapstructure:"max_age_days"`
- Compress bool `mapstructure:"compress"`
- LocalTime bool `mapstructure:"local_time"`
- }
- type LogSamplingConfig struct {
- Enabled bool `mapstructure:"enabled"`
- Initial int `mapstructure:"initial"`
- Thereafter int `mapstructure:"thereafter"`
- }
- type GeminiConfig struct {
- OAuth GeminiOAuthConfig `mapstructure:"oauth"`
- Quota GeminiQuotaConfig `mapstructure:"quota"`
- }
- type GeminiOAuthConfig struct {
- ClientID string `mapstructure:"client_id"`
- ClientSecret string `mapstructure:"client_secret"`
- Scopes string `mapstructure:"scopes"`
- }
- type GeminiQuotaConfig struct {
- Tiers map[string]GeminiTierQuotaConfig `mapstructure:"tiers"`
- Policy string `mapstructure:"policy"`
- }
- type GeminiTierQuotaConfig struct {
- ProRPD *int64 `mapstructure:"pro_rpd" json:"pro_rpd"`
- FlashRPD *int64 `mapstructure:"flash_rpd" json:"flash_rpd"`
- CooldownMinutes *int `mapstructure:"cooldown_minutes" json:"cooldown_minutes"`
- }
- type UpdateConfig struct {
- // ProxyURL 用于访问 GitHub 的代理地址
- // 支持 http/https/socks5/socks5h 协议
- // 例如: "http://127.0.0.1:7890", "socks5://127.0.0.1:1080"
- ProxyURL string `mapstructure:"proxy_url"`
- }
- type IdempotencyConfig struct {
- // ObserveOnly 为 true 时处于观察期:未携带 Idempotency-Key 的请求继续放行。
- ObserveOnly bool `mapstructure:"observe_only"`
- // DefaultTTLSeconds 关键写接口的幂等记录默认 TTL(秒)。
- DefaultTTLSeconds int `mapstructure:"default_ttl_seconds"`
- // SystemOperationTTLSeconds 系统操作接口的幂等记录 TTL(秒)。
- SystemOperationTTLSeconds int `mapstructure:"system_operation_ttl_seconds"`
- // ProcessingTimeoutSeconds processing 状态锁超时(秒)。
- ProcessingTimeoutSeconds int `mapstructure:"processing_timeout_seconds"`
- // FailedRetryBackoffSeconds 失败退避窗口(秒)。
- FailedRetryBackoffSeconds int `mapstructure:"failed_retry_backoff_seconds"`
- // MaxStoredResponseLen 持久化响应体最大长度(字节)。
- MaxStoredResponseLen int `mapstructure:"max_stored_response_len"`
- // CleanupIntervalSeconds 过期记录清理周期(秒)。
- CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"`
- // CleanupBatchSize 每次清理的最大记录数。
- CleanupBatchSize int `mapstructure:"cleanup_batch_size"`
- }
- type BatchImageConfig struct {
- Enabled bool `mapstructure:"enabled"`
- MaxItemsPerJobDefault int `mapstructure:"max_items_per_job_default"`
- MaxItemsPerJobTrial int `mapstructure:"max_items_per_job_trial"`
- MaxOutputImagesPerJob int `mapstructure:"max_output_images_per_job"`
- MaxOutputImagesPerItem int `mapstructure:"max_output_images_per_item"`
- MaxPromptCharsPerItem int `mapstructure:"max_prompt_chars_per_item"`
- MaxReferenceImagesPerJob int `mapstructure:"max_reference_images_per_job"`
- MaxReferenceInlineBytesPerJob int `mapstructure:"max_reference_inline_bytes_per_job"`
- DefaultResponseMimeType string `mapstructure:"default_response_mime_type"`
- DefaultImageSize string `mapstructure:"default_image_size"`
- MaxDownloadItemsZip int `mapstructure:"max_download_items_zip"`
- MaxDownloadBytesPerRequest int64 `mapstructure:"max_download_bytes_per_request"`
- MaxDownloadDurationSeconds int `mapstructure:"max_download_duration_seconds"`
- MaxDownloadConcurrencyPerUser int `mapstructure:"max_download_concurrency_per_user"`
- InputRetentionAfterTerminalHours int `mapstructure:"input_retention_after_terminal_hours"`
- OutputRetentionAfterTerminalHours int `mapstructure:"output_retention_after_terminal_hours"`
- OutputRetentionMaxDays int `mapstructure:"output_retention_max_days"`
- CleanupIntervalMinutes int `mapstructure:"cleanup_interval_minutes"`
- CleanupBatchSize int `mapstructure:"cleanup_batch_size"`
- QueueEnabled bool `mapstructure:"queue_enabled"`
- QueueReadyKey string `mapstructure:"queue_ready_key"`
- QueueDelayedKey string `mapstructure:"queue_delayed_key"`
- QueueActiveKey string `mapstructure:"queue_active_key"`
- InflightKeyPrefix string `mapstructure:"inflight_key_prefix"`
- LockKeyPrefix string `mapstructure:"lock_key_prefix"`
- IdempotencyKeyPrefix string `mapstructure:"idempotency_key_prefix"`
- InflightTTLSeconds int `mapstructure:"inflight_ttl_seconds"`
- JobLockTTLSeconds int `mapstructure:"job_lock_ttl_seconds"`
- DefaultRequeueDelaySeconds int `mapstructure:"default_requeue_delay_seconds"`
- ErrorRetryDelaySeconds int `mapstructure:"error_retry_delay_seconds"`
- LockConflictDelaySeconds int `mapstructure:"lock_conflict_delay_seconds"`
- StaleActiveAfterSeconds int `mapstructure:"stale_active_after_seconds"`
- DelayedMoverIntervalSeconds int `mapstructure:"delayed_mover_interval_seconds"`
- RecoveryIntervalSeconds int `mapstructure:"recovery_interval_seconds"`
- DelayedMoveLimit int `mapstructure:"delayed_move_limit"`
- RecoverLimit int `mapstructure:"recover_limit"`
- VertexEnabled bool `mapstructure:"vertex_enabled"`
- VertexProjectID string `mapstructure:"vertex_project_id"`
- VertexLocation string `mapstructure:"vertex_location"`
- // VertexManagedGCSBucket is a server-owned bucket for batch JSONL input/output.
- // Disable Cloud Storage soft delete on this bucket to avoid retaining deleted batch objects.
- VertexManagedGCSBucket string `mapstructure:"vertex_managed_gcs_bucket"`
- VertexManagedGCSPrefix string `mapstructure:"vertex_managed_gcs_prefix"`
- VertexInputRetentionHours int `mapstructure:"vertex_input_retention_hours"`
- VertexOutputRetentionHours int `mapstructure:"vertex_output_retention_hours"`
- VertexBatchPredictionBaseURL string `mapstructure:"vertex_batch_prediction_base_url"`
- VertexGCSBaseURL string `mapstructure:"vertex_gcs_base_url"`
- }
- // ImageStorageConfig 配置异步图片任务结果上传的 S3 兼容对象存储。
- // Enabled 同时作为异步图片任务功能的总开关:未启用或未配置完整凭证时,
- // 异步生图接口整体禁用,避免把上游返回的大 base64 结果塞进 Redis。
- type ImageStorageConfig struct {
- Enabled bool `mapstructure:"enabled"`
- Endpoint string `mapstructure:"endpoint"` // e.g. https://<account_id>.r2.cloudflarestorage.com
- Region string `mapstructure:"region"` // R2 用 "auto"
- Bucket string `mapstructure:"bucket"`
- AccessKeyID string `mapstructure:"access_key_id"`
- SecretAccessKey string `mapstructure:"secret_access_key"`
- Prefix string `mapstructure:"prefix"` // S3 key 前缀,如 "images/"
- ForcePathStyle bool `mapstructure:"force_path_style"` // MinIO/路径风格桶
- PublicBaseURL string `mapstructure:"public_base_url"` // 配了则返回 public_base_url/key 直链;否则 presigned
- PresignExpiry int `mapstructure:"presign_expiry_hours"` // public_base_url 为空时的 presigned 过期时长(小时)
- MaxDownloadByte int64 `mapstructure:"max_download_bytes"` // 下载上游 url 图片的字节上限
- }
- // IsConfigured 检查对象存储必要字段是否已配置
- func (c *ImageStorageConfig) IsConfigured() bool {
- return c.Bucket != "" && c.AccessKeyID != "" && c.SecretAccessKey != ""
- }
- // Active 返回异步图片任务是否可用:开关打开且凭证齐全
- func (c *ImageStorageConfig) Active() bool {
- return c.Enabled && c.IsConfigured()
- }
- // MissingCredentialKeys 返回 IsConfigured 所缺的配置键名。
- // 用于启动日志:只说"凭证不完整"会让运维以为自己漏填了,而实际可能是值填了却没被读到。
- func (c *ImageStorageConfig) MissingCredentialKeys() []string {
- var missing []string
- if c.Bucket == "" {
- missing = append(missing, "image_storage.bucket")
- }
- if c.AccessKeyID == "" {
- missing = append(missing, "image_storage.access_key_id")
- }
- if c.SecretAccessKey == "" {
- missing = append(missing, "image_storage.secret_access_key")
- }
- return missing
- }
- type LinuxDoConnectConfig struct {
- Enabled bool `mapstructure:"enabled"`
- ClientID string `mapstructure:"client_id"`
- ClientSecret string `mapstructure:"client_secret"`
- AuthorizeURL string `mapstructure:"authorize_url"`
- TokenURL string `mapstructure:"token_url"`
- UserInfoURL string `mapstructure:"userinfo_url"`
- Scopes string `mapstructure:"scopes"`
- RedirectURL string `mapstructure:"redirect_url"` // 后端回调地址(需在提供方后台登记)
- FrontendRedirectURL string `mapstructure:"frontend_redirect_url"` // 前端接收 token 的路由(默认:/auth/linuxdo/callback)
- TokenAuthMethod string `mapstructure:"token_auth_method"` // client_secret_post / client_secret_basic / none
- UsePKCE bool `mapstructure:"use_pkce"`
- // 可选:用于从 userinfo JSON 中提取字段的 gjson 路径。
- // 为空时,服务端会尝试一组常见字段名。
- UserInfoEmailPath string `mapstructure:"userinfo_email_path"`
- UserInfoIDPath string `mapstructure:"userinfo_id_path"`
- UserInfoUsernamePath string `mapstructure:"userinfo_username_path"`
- }
- type WeChatConnectConfig struct {
- Enabled bool `mapstructure:"enabled"`
- AppID string `mapstructure:"app_id"`
- AppSecret string `mapstructure:"app_secret"`
- OpenAppID string `mapstructure:"open_app_id"`
- OpenAppSecret string `mapstructure:"open_app_secret"`
- MPAppID string `mapstructure:"mp_app_id"`
- MPAppSecret string `mapstructure:"mp_app_secret"`
- MobileAppID string `mapstructure:"mobile_app_id"`
- MobileAppSecret string `mapstructure:"mobile_app_secret"`
- OpenEnabled bool `mapstructure:"open_enabled"`
- MPEnabled bool `mapstructure:"mp_enabled"`
- MobileEnabled bool `mapstructure:"mobile_enabled"`
- Mode string `mapstructure:"mode"`
- Scopes string `mapstructure:"scopes"`
- RedirectURL string `mapstructure:"redirect_url"`
- FrontendRedirectURL string `mapstructure:"frontend_redirect_url"`
- }
- type OIDCConnectConfig struct {
- Enabled bool `mapstructure:"enabled"`
- ProviderName string `mapstructure:"provider_name"` // 显示名: "Keycloak" 等
- ClientID string `mapstructure:"client_id"`
- ClientSecret string `mapstructure:"client_secret"`
- IssuerURL string `mapstructure:"issuer_url"`
- DiscoveryURL string `mapstructure:"discovery_url"`
- AuthorizeURL string `mapstructure:"authorize_url"`
- TokenURL string `mapstructure:"token_url"`
- UserInfoURL string `mapstructure:"userinfo_url"`
- JWKSURL string `mapstructure:"jwks_url"`
- Scopes string `mapstructure:"scopes"` // 默认 "openid email profile"
- RedirectURL string `mapstructure:"redirect_url"` // 后端回调地址(需在提供方后台登记)
- FrontendRedirectURL string `mapstructure:"frontend_redirect_url"` // 前端接收 token 的路由(默认:/auth/oidc/callback)
- TokenAuthMethod string `mapstructure:"token_auth_method"` // client_secret_post / client_secret_basic / none
- UsePKCE bool `mapstructure:"use_pkce"`
- ValidateIDToken bool `mapstructure:"validate_id_token"`
- UsePKCEExplicit bool `mapstructure:"-" yaml:"-"`
- ValidateIDTokenExplicit bool `mapstructure:"-" yaml:"-"`
- AllowedSigningAlgs string `mapstructure:"allowed_signing_algs"` // 默认 "RS256,ES256,PS256"
- ClockSkewSeconds int `mapstructure:"clock_skew_seconds"` // 默认 120
- RequireEmailVerified bool `mapstructure:"require_email_verified"` // 默认 false
- // 可选:用于从 userinfo JSON 中提取字段的 gjson 路径。
- // 为空时,服务端会尝试一组常见字段名。
- UserInfoEmailPath string `mapstructure:"userinfo_email_path"`
- UserInfoIDPath string `mapstructure:"userinfo_id_path"`
- UserInfoUsernamePath string `mapstructure:"userinfo_username_path"`
- }
- type DingTalkConnectConfig struct {
- Enabled bool `mapstructure:"enabled"`
- ClientID string `mapstructure:"client_id"`
- ClientSecret string `mapstructure:"client_secret"`
- AuthorizeURL string `mapstructure:"authorize_url"`
- TokenURL string `mapstructure:"token_url"`
- UserInfoURL string `mapstructure:"userinfo_url"`
- Scopes string `mapstructure:"scopes"`
- RedirectURL string `mapstructure:"redirect_url"`
- FrontendRedirectURL string `mapstructure:"frontend_redirect_url"`
- // 平台底座 + 业务行为
- DingTalkAppKind string `mapstructure:"dingtalk_app_kind"` // 仅 "internal_app"(V4 fail-closed)
- AppType string `mapstructure:"app_type"` // "public" (default) | "internal"
- // Corp 限定(none | internal_only)
- CorpRestrictionPolicy string `mapstructure:"corp_restriction_policy"`
- InternalCorpID string `mapstructure:"internal_corp_id"`
- BypassRegistration bool `mapstructure:"bypass_registration"`
- SyncCorpEmail bool `mapstructure:"sync_corp_email"`
- SyncDisplayName bool `mapstructure:"sync_display_name"`
- SyncDept bool `mapstructure:"sync_dept"`
- SyncCorpEmailAttrKey string `mapstructure:"sync_corp_email_attr_key"`
- SyncDisplayNameAttrKey string `mapstructure:"sync_display_name_attr_key"`
- SyncDeptAttrKey string `mapstructure:"sync_dept_attr_key"`
- SyncCorpEmailAttrName string `mapstructure:"sync_corp_email_attr_name"`
- SyncDisplayNameAttrName string `mapstructure:"sync_display_name_attr_name"`
- SyncDeptAttrName string `mapstructure:"sync_dept_attr_name"`
- // 邮箱 + Username
- RequireEmail bool `mapstructure:"require_email"`
- UsernameOverwritePolicy string `mapstructure:"username_overwrite_policy"`
- // Attribute(私有版扩展点;开源版仅声明)
- UsernameAttributeKey string `mapstructure:"username_attribute_key"`
- EnableAttributeMatching bool `mapstructure:"enable_attribute_matching"`
- EnableAttributeSync bool `mapstructure:"enable_attribute_sync"`
- AttributeSyncFields []string `mapstructure:"attribute_sync_fields"`
- AttributeSyncOverwritePolicy string `mapstructure:"attribute_sync_overwrite_policy"`
- }
- type EmailOAuthProviderConfig struct {
- Enabled bool `mapstructure:"enabled"`
- ClientID string `mapstructure:"client_id"`
- ClientSecret string `mapstructure:"client_secret"`
- AuthorizeURL string `mapstructure:"authorize_url"`
- TokenURL string `mapstructure:"token_url"`
- UserInfoURL string `mapstructure:"userinfo_url"`
- EmailsURL string `mapstructure:"emails_url"`
- Scopes string `mapstructure:"scopes"`
- RedirectURL string `mapstructure:"redirect_url"`
- FrontendRedirectURL string `mapstructure:"frontend_redirect_url"`
- }
- const (
- defaultWeChatConnectMode = "open"
- defaultWeChatConnectScopes = "snsapi_login"
- defaultWeChatConnectFrontendRedirect = "/auth/wechat/callback"
- )
- func firstNonEmptyString(values ...string) string {
- for _, value := range values {
- if trimmed := strings.TrimSpace(value); trimmed != "" {
- return trimmed
- }
- }
- return ""
- }
- func normalizeWeChatConnectMode(raw string) string {
- switch strings.ToLower(strings.TrimSpace(raw)) {
- case "mp":
- return "mp"
- case "mobile":
- return "mobile"
- default:
- return defaultWeChatConnectMode
- }
- }
- func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled bool, mode string) string {
- mode = normalizeWeChatConnectMode(mode)
- switch mode {
- case "open":
- if openEnabled {
- return "open"
- }
- case "mp":
- if mpEnabled {
- return "mp"
- }
- case "mobile":
- if mobileEnabled {
- return "mobile"
- }
- }
- switch {
- case openEnabled:
- return "open"
- case mpEnabled:
- return "mp"
- case mobileEnabled:
- return "mobile"
- default:
- return mode
- }
- }
- func defaultWeChatConnectScopesForMode(mode string) string {
- switch normalizeWeChatConnectMode(mode) {
- case "mp":
- return "snsapi_userinfo"
- case "mobile":
- return ""
- default:
- return defaultWeChatConnectScopes
- }
- }
- func normalizeWeChatConnectScopes(raw, mode string) string {
- switch normalizeWeChatConnectMode(mode) {
- case "mp":
- switch strings.TrimSpace(raw) {
- case "snsapi_base":
- return "snsapi_base"
- case "snsapi_userinfo":
- return "snsapi_userinfo"
- default:
- return defaultWeChatConnectScopesForMode(mode)
- }
- case "mobile":
- return ""
- default:
- return defaultWeChatConnectScopes
- }
- }
- func shouldApplyLegacyWeChatEnv(configKey, envKey string) bool {
- if viper.InConfig(configKey) {
- return false
- }
- _, hasNewEnv := os.LookupEnv(envKey)
- return !hasNewEnv
- }
- func hasExplicitConfigOrEnv(configKey, envKey string) bool {
- if viper.InConfig(configKey) {
- return true
- }
- _, ok := os.LookupEnv(envKey)
- return ok
- }
- func applyLegacyWeChatConnectEnvCompatibility(cfg *WeChatConnectConfig) {
- if cfg == nil {
- return
- }
- legacyOpenAppID := ""
- if shouldApplyLegacyWeChatEnv("wechat_connect.open_app_id", "WECHAT_CONNECT_OPEN_APP_ID") &&
- shouldApplyLegacyWeChatEnv("wechat_connect.app_id", "WECHAT_CONNECT_APP_ID") {
- legacyOpenAppID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_ID"))
- if legacyOpenAppID != "" {
- cfg.OpenAppID = legacyOpenAppID
- }
- }
- legacyOpenAppSecret := ""
- if shouldApplyLegacyWeChatEnv("wechat_connect.open_app_secret", "WECHAT_CONNECT_OPEN_APP_SECRET") &&
- shouldApplyLegacyWeChatEnv("wechat_connect.app_secret", "WECHAT_CONNECT_APP_SECRET") {
- legacyOpenAppSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_SECRET"))
- if legacyOpenAppSecret != "" {
- cfg.OpenAppSecret = legacyOpenAppSecret
- }
- }
- legacyMPAppID := ""
- if shouldApplyLegacyWeChatEnv("wechat_connect.mp_app_id", "WECHAT_CONNECT_MP_APP_ID") &&
- shouldApplyLegacyWeChatEnv("wechat_connect.app_id", "WECHAT_CONNECT_APP_ID") {
- legacyMPAppID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID"))
- if legacyMPAppID != "" {
- cfg.MPAppID = legacyMPAppID
- }
- }
- legacyMPAppSecret := ""
- if shouldApplyLegacyWeChatEnv("wechat_connect.mp_app_secret", "WECHAT_CONNECT_MP_APP_SECRET") &&
- shouldApplyLegacyWeChatEnv("wechat_connect.app_secret", "WECHAT_CONNECT_APP_SECRET") {
- legacyMPAppSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET"))
- if legacyMPAppSecret != "" {
- cfg.MPAppSecret = legacyMPAppSecret
- }
- }
- if shouldApplyLegacyWeChatEnv("wechat_connect.frontend_redirect_url", "WECHAT_CONNECT_FRONTEND_REDIRECT_URL") {
- if legacyFrontend := strings.TrimSpace(os.Getenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL")); legacyFrontend != "" {
- cfg.FrontendRedirectURL = legacyFrontend
- }
- }
- hasLegacyOpen := legacyOpenAppID != "" && legacyOpenAppSecret != ""
- hasLegacyMP := legacyMPAppID != "" && legacyMPAppSecret != ""
- if shouldApplyLegacyWeChatEnv("wechat_connect.enabled", "WECHAT_CONNECT_ENABLED") && (hasLegacyOpen || hasLegacyMP) {
- cfg.Enabled = true
- }
- if shouldApplyLegacyWeChatEnv("wechat_connect.open_enabled", "WECHAT_CONNECT_OPEN_ENABLED") && hasLegacyOpen {
- cfg.OpenEnabled = true
- }
- if shouldApplyLegacyWeChatEnv("wechat_connect.mp_enabled", "WECHAT_CONNECT_MP_ENABLED") && hasLegacyMP {
- cfg.MPEnabled = true
- }
- if shouldApplyLegacyWeChatEnv("wechat_connect.mode", "WECHAT_CONNECT_MODE") {
- switch {
- case hasLegacyMP && !hasLegacyOpen:
- cfg.Mode = "mp"
- case hasLegacyOpen:
- cfg.Mode = "open"
- }
- }
- if shouldApplyLegacyWeChatEnv("wechat_connect.scopes", "WECHAT_CONNECT_SCOPES") {
- switch {
- case hasLegacyMP && !hasLegacyOpen:
- cfg.Scopes = defaultWeChatConnectScopesForMode("mp")
- case hasLegacyOpen:
- cfg.Scopes = defaultWeChatConnectScopesForMode("open")
- }
- }
- }
- func normalizeWeChatConnectConfig(cfg *WeChatConnectConfig) {
- if cfg == nil {
- return
- }
- cfg.AppID = strings.TrimSpace(cfg.AppID)
- cfg.AppSecret = strings.TrimSpace(cfg.AppSecret)
- cfg.OpenAppID = strings.TrimSpace(cfg.OpenAppID)
- cfg.OpenAppSecret = strings.TrimSpace(cfg.OpenAppSecret)
- cfg.MPAppID = strings.TrimSpace(cfg.MPAppID)
- cfg.MPAppSecret = strings.TrimSpace(cfg.MPAppSecret)
- cfg.MobileAppID = strings.TrimSpace(cfg.MobileAppID)
- cfg.MobileAppSecret = strings.TrimSpace(cfg.MobileAppSecret)
- cfg.Mode = normalizeWeChatConnectMode(cfg.Mode)
- cfg.RedirectURL = strings.TrimSpace(cfg.RedirectURL)
- cfg.FrontendRedirectURL = strings.TrimSpace(cfg.FrontendRedirectURL)
- cfg.AppID = firstNonEmptyString(cfg.AppID, cfg.OpenAppID, cfg.MPAppID, cfg.MobileAppID)
- cfg.AppSecret = firstNonEmptyString(cfg.AppSecret, cfg.OpenAppSecret, cfg.MPAppSecret, cfg.MobileAppSecret)
- cfg.OpenAppID = firstNonEmptyString(cfg.OpenAppID, cfg.AppID)
- cfg.OpenAppSecret = firstNonEmptyString(cfg.OpenAppSecret, cfg.AppSecret)
- cfg.MPAppID = firstNonEmptyString(cfg.MPAppID, cfg.AppID)
- cfg.MPAppSecret = firstNonEmptyString(cfg.MPAppSecret, cfg.AppSecret)
- cfg.MobileAppID = firstNonEmptyString(cfg.MobileAppID, cfg.AppID)
- cfg.MobileAppSecret = firstNonEmptyString(cfg.MobileAppSecret, cfg.AppSecret)
- if !cfg.OpenEnabled && !cfg.MPEnabled && !cfg.MobileEnabled && cfg.Enabled {
- switch cfg.Mode {
- case "mp":
- cfg.MPEnabled = true
- case "mobile":
- cfg.MobileEnabled = true
- default:
- cfg.OpenEnabled = true
- }
- }
- cfg.Mode = normalizeWeChatConnectStoredMode(cfg.OpenEnabled, cfg.MPEnabled, cfg.MobileEnabled, cfg.Mode)
- cfg.Scopes = normalizeWeChatConnectScopes(cfg.Scopes, cfg.Mode)
- if cfg.FrontendRedirectURL == "" {
- cfg.FrontendRedirectURL = defaultWeChatConnectFrontendRedirect
- }
- }
- // TokenRefreshConfig OAuth token自动刷新配置
- type TokenRefreshConfig struct {
- // 是否启用自动刷新
- Enabled bool `mapstructure:"enabled"`
- // 检查间隔(分钟)
- CheckIntervalMinutes int `mapstructure:"check_interval_minutes"`
- // 提前刷新时间(小时),在token过期前多久开始刷新
- RefreshBeforeExpiryHours float64 `mapstructure:"refresh_before_expiry_hours"`
- // 最大重试次数
- MaxRetries int `mapstructure:"max_retries"`
- // 重试退避基础时间(秒)
- RetryBackoffSeconds int `mapstructure:"retry_backoff_seconds"`
- // 每次从数据库读取的候选账号上限
- CandidatePageSize int `mapstructure:"candidate_page_size"`
- // 每个平台允许的并发刷新数
- ProviderConcurrency int `mapstructure:"provider_concurrency"`
- // 每个平台、每个进程允许的刷新请求速率
- ProviderQPS int `mapstructure:"provider_qps"`
- // 一个周期内连续临时失败达到此值后停止该平台
- ProviderFailureThreshold int `mapstructure:"provider_failure_threshold"`
- // 单次上游刷新尝试的超时(秒)
- AttemptTimeoutSeconds int `mapstructure:"attempt_timeout_seconds"`
- // 单个后台刷新周期的总超时(秒)
- CycleTimeoutSeconds int `mapstructure:"cycle_timeout_seconds"`
- }
- type PricingConfig struct {
- // 价格数据远程URL(默认使用LiteLLM镜像)
- RemoteURL string `mapstructure:"remote_url"`
- // 哈希校验文件URL
- HashURL string `mapstructure:"hash_url"`
- // 本地数据目录
- DataDir string `mapstructure:"data_dir"`
- // 回退文件路径
- FallbackFile string `mapstructure:"fallback_file"`
- // 覆盖补丁文件路径(可选):条目按字段浅合并覆盖目录/回退数据,优先级最高
- OverrideFile string `mapstructure:"override_file"`
- // 更新间隔(小时)
- UpdateIntervalHours int `mapstructure:"update_interval_hours"`
- // 哈希校验间隔(分钟)
- HashCheckIntervalMinutes int `mapstructure:"hash_check_interval_minutes"`
- }
- type ServerConfig struct {
- Host string `mapstructure:"host"`
- Port int `mapstructure:"port"`
- Mode string `mapstructure:"mode"` // debug/release
- EnableServerTiming bool `mapstructure:"enable_server_timing"` // Admin UI Server-Timing response header
- FrontendURL string `mapstructure:"frontend_url"` // 前端基础 URL,用于生成邮件中的外部链接
- ReadHeaderTimeout int `mapstructure:"read_header_timeout"` // 读取请求头超时(秒)
- MaxHeaderBytes int `mapstructure:"max_header_bytes"` // 请求头最大字节数(HTTP/2 映射为 header-list 上限)
- IdleTimeout int `mapstructure:"idle_timeout"` // 空闲连接超时(秒)
- TrustedProxies []string `mapstructure:"trusted_proxies"` // 可信代理列表(CIDR/IP)
- TrustedProxiesConfigured bool `mapstructure:"-" json:"-" yaml:"-"` // 是否显式配置了可信代理列表
- MaxRequestBodySize int64 `mapstructure:"max_request_body_size"` // 全局最大请求体限制
- H2C H2CConfig `mapstructure:"h2c"` // HTTP/2 Cleartext 配置
- }
- // H2CConfig HTTP/2 Cleartext 配置
- type H2CConfig struct {
- Enabled bool `mapstructure:"enabled"` // 是否启用 H2C
- MaxConcurrentStreams uint32 `mapstructure:"max_concurrent_streams"` // 最大并发流数量
- IdleTimeout int `mapstructure:"idle_timeout"` // 空闲超时(秒)
- MaxReadFrameSize int `mapstructure:"max_read_frame_size"` // 最大帧大小(字节)
- MaxUploadBufferPerConnection int `mapstructure:"max_upload_buffer_per_connection"` // 每个连接的上传缓冲区(字节)
- MaxUploadBufferPerStream int `mapstructure:"max_upload_buffer_per_stream"` // 每个流的上传缓冲区(字节)
- }
- type CORSConfig struct {
- AllowedOrigins []string `mapstructure:"allowed_origins"`
- AllowCredentials bool `mapstructure:"allow_credentials"`
- }
- // WebAuthnConfig configures this deployment as a WebAuthn relying party.
- // RPID and RPOrigins are security boundaries and must never be inferred from
- // untrusted request Host or Origin headers.
- type WebAuthnConfig struct {
- Enabled bool `mapstructure:"enabled"`
- RPDisplayName string `mapstructure:"rp_display_name"`
- RPID string `mapstructure:"rp_id"`
- RPOrigins []string `mapstructure:"rp_origins"`
- }
- const MaxForwardedClientIPHeaders = 16
- type ForwardedClientIPSettings struct {
- TrustForwardedIP bool
- Headers []string
- }
- type SecurityConfig struct {
- URLAllowlist URLAllowlistConfig `mapstructure:"url_allowlist"`
- ResponseHeaders ResponseHeaderConfig `mapstructure:"response_headers"`
- CSP CSPConfig `mapstructure:"csp"`
- ProxyFallback ProxyFallbackConfig `mapstructure:"proxy_fallback"`
- ProxyProbe ProxyProbeConfig `mapstructure:"proxy_probe"`
- // TrustForwardedIPForAPIKeyACL enables legacy raw forwarded-header takeover.
- // When disabled, server.trusted_proxies is authoritative for all client-IP consumers.
- TrustForwardedIPForAPIKeyACL bool `mapstructure:"trust_forwarded_ip_for_api_key_acl"`
- ForwardedClientIPHeaders []string `mapstructure:"forwarded_client_ip_headers" json:"forwarded_client_ip_headers" yaml:"forwarded_client_ip_headers"`
- forwardedClientIPSettingsLive *atomic.Pointer[ForwardedClientIPSettings] `mapstructure:"-" json:"-" yaml:"-"`
- }
- func NormalizeForwardedClientIPHeaders(headers []string) ([]string, error) {
- normalized := make([]string, 0, len(headers))
- seen := make(map[string]struct{}, len(headers))
- for _, header := range headers {
- header = strings.TrimSpace(header)
- if !httpguts.ValidHeaderFieldName(header) {
- return nil, fmt.Errorf("invalid HTTP header field name %q", header)
- }
- canonical := textproto.CanonicalMIMEHeaderKey(header)
- key := strings.ToLower(canonical)
- if _, exists := seen[key]; exists {
- continue
- }
- if len(normalized) == MaxForwardedClientIPHeaders {
- return nil, fmt.Errorf("forwarded client IP headers must contain at most %d unique names", MaxForwardedClientIPHeaders)
- }
- seen[key] = struct{}{}
- normalized = append(normalized, canonical)
- }
- return normalized, nil
- }
- func cloneForwardedClientIPHeaders(headers []string) []string {
- if len(headers) == 0 {
- return []string{}
- }
- return append([]string(nil), headers...)
- }
- func (c *Config) ForwardedClientIPSettings() ForwardedClientIPSettings {
- if c == nil {
- return ForwardedClientIPSettings{Headers: []string{}}
- }
- live := c.Security.forwardedClientIPSettingsLive
- if live != nil {
- if snapshot := live.Load(); snapshot != nil {
- return ForwardedClientIPSettings{
- TrustForwardedIP: snapshot.TrustForwardedIP,
- Headers: cloneForwardedClientIPHeaders(snapshot.Headers),
- }
- }
- }
- return ForwardedClientIPSettings{
- TrustForwardedIP: c.Security.TrustForwardedIPForAPIKeyACL,
- Headers: cloneForwardedClientIPHeaders(c.Security.ForwardedClientIPHeaders),
- }
- }
- func (c *Config) TrustForwardedIPForAPIKeyACL() bool {
- return c.ForwardedClientIPSettings().TrustForwardedIP
- }
- // ForwardedClientIPTrustEnabled reports whether the legacy forwarded-header
- // compatibility mode currently overrides server.trusted_proxies.
- func (c *Config) ForwardedClientIPTrustEnabled() bool {
- return c != nil && c.TrustForwardedIPForAPIKeyACL()
- }
- func (c *Config) SetForwardedClientIPSettings(enabled bool, headers []string) {
- if c == nil {
- return
- }
- headers = cloneForwardedClientIPHeaders(headers)
- if c.Security.forwardedClientIPSettingsLive == nil {
- c.Security.forwardedClientIPSettingsLive = &atomic.Pointer[ForwardedClientIPSettings]{}
- }
- c.Security.forwardedClientIPSettingsLive.Store(&ForwardedClientIPSettings{
- TrustForwardedIP: enabled,
- Headers: headers,
- })
- }
- func (c *Config) SetTrustForwardedIPForAPIKeyACL(enabled bool) {
- if c == nil {
- return
- }
- c.SetForwardedClientIPSettings(enabled, c.ForwardedClientIPSettings().Headers)
- }
- type URLAllowlistConfig struct {
- Enabled bool `mapstructure:"enabled"`
- UpstreamHosts []string `mapstructure:"upstream_hosts"`
- PricingHosts []string `mapstructure:"pricing_hosts"`
- CRSHosts []string `mapstructure:"crs_hosts"`
- AllowPrivateHosts bool `mapstructure:"allow_private_hosts"`
- // 关闭 URL 白名单校验时,是否允许 http URL(默认只允许 https)
- AllowInsecureHTTP bool `mapstructure:"allow_insecure_http"`
- }
- type ResponseHeaderConfig struct {
- Enabled bool `mapstructure:"enabled"`
- AdditionalAllowed []string `mapstructure:"additional_allowed"`
- ForceRemove []string `mapstructure:"force_remove"`
- }
- type CSPConfig struct {
- Enabled bool `mapstructure:"enabled"`
- Policy string `mapstructure:"policy"`
- }
- type ProxyFallbackConfig struct {
- // AllowDirectOnError 当辅助服务的代理初始化失败时是否允许回退直连。
- // 仅影响以下非 AI 账号连接的辅助服务:
- // - GitHub Release 更新检查
- // - 定价数据拉取
- // 不影响 AI 账号网关连接(Claude/OpenAI/Gemini/Antigravity),
- // 这些关键路径的代理失败始终返回错误,不会回退直连。
- // 默认 false:避免因代理配置错误导致服务器真实 IP 泄露。
- AllowDirectOnError bool `mapstructure:"allow_direct_on_error"`
- }
- type ProxyProbeConfig struct {
- InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` // 已禁用:禁止跳过 TLS 证书验证
- // URLs 按优先级排列的自定义探测 URL 列表。
- // 留空时使用内置默认列表(ip-api → ipify)。
- // 某些 AI API 专用代理只允许访问特定域名,配置多个备选可提高探测成功率。
- URLs []ProbeURLConfig `mapstructure:"urls"`
- }
- // ProbeURLConfig 描述一个探测端点及其响应解析方式。
- type ProbeURLConfig struct {
- URL string `mapstructure:"url"`
- Parser string `mapstructure:"parser"` // "ip-api" / "ipify" / "chatgpt-trace"
- }
- func normalizeProxyProbeURLs(targets []ProbeURLConfig) ([]ProbeURLConfig, error) {
- if len(targets) == 0 {
- return nil, nil
- }
- normalized := make([]ProbeURLConfig, 0, len(targets))
- for i, target := range targets {
- rawURL := strings.TrimSpace(target.URL)
- parser := strings.ToLower(strings.TrimSpace(target.Parser))
- if rawURL == "" {
- return nil, fmt.Errorf("entry %d: url is required", i)
- }
- if parser == "" {
- return nil, fmt.Errorf("entry %d: parser is required", i)
- }
- switch parser {
- case "ip-api", "ipify", "chatgpt-trace":
- default:
- return nil, fmt.Errorf("entry %d: unsupported parser %q", i, target.Parser)
- }
- parsed, err := url.Parse(rawURL)
- if err != nil || parsed.Host == "" {
- return nil, fmt.Errorf("entry %d: invalid url %q", i, target.URL)
- }
- if parsed.Scheme != "http" && parsed.Scheme != "https" {
- return nil, fmt.Errorf("entry %d: url scheme must be http or https", i)
- }
- normalized = append(normalized, ProbeURLConfig{
- URL: rawURL,
- Parser: parser,
- })
- }
- return normalized, nil
- }
- type BillingConfig struct {
- CircuitBreaker CircuitBreakerConfig `mapstructure:"circuit_breaker"`
- // MinimumBalanceReserve is the conservative preflight floor for balance billing.
- // Requests in balance mode are rejected when the cached balance is below this
- // amount, even if it is still positive. Set to 0 to keep the legacy balance > 0 gate.
- MinimumBalanceReserve float64 `mapstructure:"minimum_balance_reserve"`
- // UserPlatformQuotaCacheTTLSeconds 用户 × 平台 quota 缓存 TTL(秒),默认 86400=1天,覆盖典型 daily 窗口。
- // 消费点:
- // - billing_cache_service.cacheWriteWorker 异步累加
- // - billing_cache_service.checkUserPlatformQuotaEligibility 首次缓存装载
- // 读写两端必须共用同一 TTL,避免缓存生命周期不一致导致 quota 计数漂移。
- UserPlatformQuotaCacheTTLSeconds int `mapstructure:"user_platform_quota_cache_ttl_seconds"`
- // UserPlatformQuotaSentinelTTLSeconds sentinel(无 limit 占位)entry 的 TTL,
- // 显著短于 quota cache 默认 86400s 以控 Redis 内存;默认 3600=1h。
- UserPlatformQuotaSentinelTTLSeconds int `mapstructure:"user_platform_quota_sentinel_ttl_seconds"`
- }
- type CircuitBreakerConfig struct {
- Enabled bool `mapstructure:"enabled"`
- FailureThreshold int `mapstructure:"failure_threshold"`
- ResetTimeoutSeconds int `mapstructure:"reset_timeout_seconds"`
- HalfOpenRequests int `mapstructure:"half_open_requests"`
- }
- type ConcurrencyConfig struct {
- // PingInterval: 并发等待期间的 SSE ping 间隔(秒)
- PingInterval int `mapstructure:"ping_interval"`
- }
- type ImageConcurrencyConfig struct {
- // Enabled: 是否启用图片生成独立并发限制,默认关闭以保持现有行为
- Enabled bool `mapstructure:"enabled"`
- // MaxConcurrentRequests: 当前进程允许同时处理的图片生成请求数,0表示不限制
- MaxConcurrentRequests int `mapstructure:"max_concurrent_requests"`
- // OverflowMode: 图片并发达到上限后的处理方式:reject/wait
- OverflowMode string `mapstructure:"overflow_mode"`
- // WaitTimeoutSeconds: overflow_mode=wait 时等待图片并发槽位的超时时间(秒)
- WaitTimeoutSeconds int `mapstructure:"wait_timeout_seconds"`
- // MaxWaitingRequests: overflow_mode=wait 时当前进程允许排队等待的图片请求数
- MaxWaitingRequests int `mapstructure:"max_waiting_requests"`
- }
- const (
- ImageConcurrencyOverflowModeReject = "reject"
- ImageConcurrencyOverflowModeWait = "wait"
- )
- // GatewayConfig API网关相关配置
- type GatewayConfig struct {
- // 等待上游响应头的超时时间(秒),0表示无超时
- // 注意:这不影响流式数据传输,只控制等待响应头的时间
- ResponseHeaderTimeout int `mapstructure:"response_header_timeout"`
- // OpenAIResponseHeaderTimeout: OpenAI/Codex 上游等待响应头的超时时间(秒),0表示无超时
- // OpenAI/Codex 请求可能在上游排队较久;默认不使用通用响应头超时截断。
- OpenAIResponseHeaderTimeout int `mapstructure:"openai_response_header_timeout"`
- // GrokResponseHeaderTimeout bounds the pre-first-byte wait for xAI/Grok.
- // A zero value uses the provider-safe default instead of the generic gateway timeout.
- GrokResponseHeaderTimeout int `mapstructure:"grok_response_header_timeout"`
- // OpenAIFirstOutputTimeoutSeconds: native HTTP Responses 首个语义输出超时(秒),0表示禁用。
- OpenAIFirstOutputTimeoutSeconds int `mapstructure:"openai_first_output_timeout_seconds"`
- // OpenAIHighEffortFirstOutputTimeoutSeconds: high/xhigh/max 推理的首个语义输出超时(秒)。
- // 0 表示回退到 OpenAIFirstOutputTimeoutSeconds。
- OpenAIHighEffortFirstOutputTimeoutSeconds int `mapstructure:"openai_high_effort_first_output_timeout_seconds"`
- // 请求体最大字节数,用于网关请求体大小限制
- MaxBodySize int64 `mapstructure:"max_body_size"`
- // TextMaxBodySize limits endpoints that cannot carry inline image/video payloads.
- TextMaxBodySize int64 `mapstructure:"text_max_body_size"`
- // 非流式上游响应体读取上限(字节),用于防止无界读取导致内存放大
- UpstreamResponseReadMaxBytes int64 `mapstructure:"upstream_response_read_max_bytes"`
- // 上游模型列表响应体读取上限(字节)
- ModelsListReadMaxBytes int64 `mapstructure:"models_list_read_max_bytes"`
- // 代理探测响应体读取上限(字节)
- ProxyProbeResponseReadMaxBytes int64 `mapstructure:"proxy_probe_response_read_max_bytes"`
- // Gemini 上游响应头调试日志开关(默认关闭,避免高频日志开销)
- GeminiDebugResponseHeaders bool `mapstructure:"gemini_debug_response_headers"`
- // ConnectionPoolIsolation: 上游连接池隔离策略(proxy/account/account_proxy)
- ConnectionPoolIsolation string `mapstructure:"connection_pool_isolation"`
- // ForceCodexCLI: 强制将 OpenAI `/v1/responses` 请求按 Codex CLI 处理。
- // 用于网关未透传/改写 User-Agent 时的兼容兜底(默认关闭,避免影响其他客户端)。
- ForceCodexCLI bool `mapstructure:"force_codex_cli"`
- // DisableCodexIdentityEnforcement: 关闭「强制统一 Codex 出站身份」。上游 /backend-api/codex
- // 在容量紧张时按客户端身份分优先级降载,被降载的请求会拿到 HTTP 200 + 流内
- // server_is_overloaded,该次请求失败。默认强制统一出口:所有 OAuth 出站的
- // User-Agent / originator / version 都改写为网关规范身份,确保没有请求带着第三方或陈旧身份
- // 出站。置 true 后退回「仅按最终 User-Agent 配对 originator」的收口语义,供上游策略变动时回滚。
- //
- // 取反义命名是为了让零值安全:该开关会发布为进程级快照,未经 viper 加载而手工构造的
- // Config(测试、工具)其零值必须落在「强制统一开启」这一侧,否则会静默丢掉这层保护。
- DisableCodexIdentityEnforcement bool `mapstructure:"disable_codex_identity_enforcement"`
- // DisableCodexOriginatorNormalization: 已废弃,等价于 DisableCodexIdentityEnforcement。
- // 保留以兼容既有配置文件;加载时会折叠进新键,不要在新代码里直接读取。
- DisableCodexOriginatorNormalization bool `mapstructure:"disable_codex_originator_normalization"`
- // CodexImageGenerationBridgeEnabled: 是否为 Codex `/v1/responses` 自动注入 image_generation 工具和桥接指令。
- // 默认关闭,避免纯文本 Codex 请求被意外改写;显式携带 image_generation 工具的请求仍按分组能力转发。
- CodexImageGenerationBridgeEnabled bool `mapstructure:"codex_image_generation_bridge_enabled"`
- // ForcedCodexInstructionsTemplateFile: 服务端强制附加到 Codex 顶层 instructions 的模板文件路径。
- // 模板渲染后会直接覆盖最终 instructions;若需要保留客户端 system 转换结果,请在模板中显式引用 {{ .ExistingInstructions }}。
- ForcedCodexInstructionsTemplateFile string `mapstructure:"forced_codex_instructions_template_file"`
- // ForcedCodexInstructionsTemplate: 启动时从模板文件读取并缓存的模板内容。
- // 该字段不直接参与配置反序列化,仅用于请求热路径避免重复读盘。
- ForcedCodexInstructionsTemplate string `mapstructure:"-"`
- // OpenAIPassthroughAllowTimeoutHeaders: OpenAI 透传模式是否放行客户端超时头
- // 关闭(默认)可避免 x-stainless-timeout 等头导致上游提前断流。
- OpenAIPassthroughAllowTimeoutHeaders bool `mapstructure:"openai_passthrough_allow_timeout_headers"`
- // OpenAICompactModel: /responses/compact 上游使用的模型。
- // compact 端点支持模型滞后于普通 /responses 时,可用该配置降级规避上游错误。
- OpenAICompactModel string `mapstructure:"openai_compact_model"`
- // OpenAIWS: OpenAI Responses WebSocket 配置(默认开启,可按需回滚到 HTTP)
- OpenAIWS GatewayOpenAIWSConfig `mapstructure:"openai_ws"`
- // Live: ChatGPT Frameless Live 会话配置。
- Live GatewayLiveConfig `mapstructure:"live"`
- // OpenAIScheduler: OpenAI 高级调度器粘性逃逸配置
- OpenAIScheduler GatewayOpenAISchedulerConfig `mapstructure:"openai_scheduler"`
- // OpenAIHTTP2: OpenAI HTTP 上游协议策略(默认启用 HTTP/2,可按代理能力回退 HTTP/1.1)
- OpenAIHTTP2 GatewayOpenAIHTTP2Config `mapstructure:"openai_http2"`
- // OpenAIProxyStreamCircuit: Responses SSE 代理断流熔断策略。
- OpenAIProxyStreamCircuit GatewayOpenAIProxyStreamCircuitConfig `mapstructure:"openai_proxy_stream_circuit"`
- // ImageConcurrency: 图片生成独立并发限制配置(默认关闭)
- ImageConcurrency ImageConcurrencyConfig `mapstructure:"image_concurrency"`
- // HTTP 上游连接池配置(性能优化:支持高并发场景调优)
- // MaxIdleConns: 所有主机的最大空闲连接总数
- MaxIdleConns int `mapstructure:"max_idle_conns"`
- // MaxIdleConnsPerHost: 每个主机的最大空闲连接数(关键参数,影响连接复用率)
- MaxIdleConnsPerHost int `mapstructure:"max_idle_conns_per_host"`
- // MaxConnsPerHost: 每个主机的最大连接数(包括活跃+空闲),0表示无限制
- MaxConnsPerHost int `mapstructure:"max_conns_per_host"`
- // IdleConnTimeoutSeconds: 空闲连接超时时间(秒)
- IdleConnTimeoutSeconds int `mapstructure:"idle_conn_timeout_seconds"`
- // MaxUpstreamClients: 上游连接池客户端最大缓存数量
- // 当使用连接池隔离策略时,系统会为不同的账户/代理组合创建独立的 HTTP 客户端
- // 此参数限制缓存的客户端数量,超出后会淘汰最久未使用的客户端
- // 建议值:预估的活跃账户数 * 1.2(留有余量)
- MaxUpstreamClients int `mapstructure:"max_upstream_clients"`
- // ClientIdleTTLSeconds: 上游连接池客户端空闲回收阈值(秒)
- // 超过此时间未使用的客户端会被标记为可回收
- // 建议值:根据用户访问频率设置,一般 10-30 分钟
- ClientIdleTTLSeconds int `mapstructure:"client_idle_ttl_seconds"`
- // ConcurrencySlotTTLMinutes: 并发槽位过期时间(分钟)
- // 应大于最长 LLM 请求时间,防止请求完成前槽位过期
- ConcurrencySlotTTLMinutes int `mapstructure:"concurrency_slot_ttl_minutes"`
- // SessionIdleTimeoutMinutes: 会话空闲超时时间(分钟),默认 5 分钟
- // 用于 Anthropic OAuth/SetupToken 账号的会话数量限制功能
- // 空闲超过此时间的会话将被自动释放
- SessionIdleTimeoutMinutes int `mapstructure:"session_idle_timeout_minutes"`
- // StreamDataIntervalTimeout: 流数据间隔超时(秒),0表示禁用
- StreamDataIntervalTimeout int `mapstructure:"stream_data_interval_timeout"`
- // StreamKeepaliveInterval: 流式 keepalive 间隔(秒),0表示禁用
- StreamKeepaliveInterval int `mapstructure:"stream_keepalive_interval"`
- // ImageStreamDataIntervalTimeout: 图片流数据间隔超时(秒),0表示禁用
- ImageStreamDataIntervalTimeout int `mapstructure:"image_stream_data_interval_timeout"`
- // ImageStreamKeepaliveInterval: 图片流式 keepalive 间隔(秒),0表示禁用
- ImageStreamKeepaliveInterval int `mapstructure:"image_stream_keepalive_interval"`
- // ImageNonstreamKeepaliveInterval: 图片非流式 JSON keepalive 间隔(秒),0表示禁用
- ImageNonstreamKeepaliveInterval int `mapstructure:"image_nonstream_keepalive_interval"`
- // MaxLineSize: 上游 SSE 单行最大字节数(0使用默认值)
- MaxLineSize int `mapstructure:"max_line_size"`
- // 是否记录上游错误响应体摘要(避免输出请求内容)
- LogUpstreamErrorBody bool `mapstructure:"log_upstream_error_body"`
- // 上游错误响应体记录最大字节数(超过会截断)
- LogUpstreamErrorBodyMaxBytes int `mapstructure:"log_upstream_error_body_max_bytes"`
- // API-key 账号在客户端未提供 anthropic-beta 时,是否按需自动补齐(默认关闭以保持兼容)
- InjectBetaForAPIKey bool `mapstructure:"inject_beta_for_apikey"`
- // 是否允许对部分 400 错误触发 failover(默认关闭以避免改变语义)
- FailoverOn400 bool `mapstructure:"failover_on_400"`
- // 账户切换最大次数(遇到上游错误时切换到其他账户的次数上限)
- MaxAccountSwitches int `mapstructure:"max_account_switches"`
- // Gemini 账户切换最大次数(Gemini 平台单独配置,因 API 限制更严格)
- MaxAccountSwitchesGemini int `mapstructure:"max_account_switches_gemini"`
- // Antigravity 429 fallback 限流时间(分钟),解析重置时间失败时使用
- AntigravityFallbackCooldownMinutes int `mapstructure:"antigravity_fallback_cooldown_minutes"`
- // Scheduling: 账号调度相关配置
- Scheduling GatewaySchedulingConfig `mapstructure:"scheduling"`
- // TLSFingerprint: TLS指纹伪装配置
- TLSFingerprint TLSFingerprintConfig `mapstructure:"tls_fingerprint"`
- // UsageRecord: 使用量记录异步队列配置(有界队列 + 固定 worker)
- UsageRecord GatewayUsageRecordConfig `mapstructure:"usage_record"`
- // UserGroupRateCacheTTLSeconds: 用户分组倍率热路径缓存 TTL(秒)
- UserGroupRateCacheTTLSeconds int `mapstructure:"user_group_rate_cache_ttl_seconds"`
- // ModelsListCacheTTLSeconds: /v1/models 模型列表短缓存 TTL(秒)
- ModelsListCacheTTLSeconds int `mapstructure:"models_list_cache_ttl_seconds"`
- // UserMessageQueue: 用户消息串行队列配置
- // 对 role:"user" 的真实用户消息实施账号级串行化 + RPM 自适应延迟
- UserMessageQueue UserMessageQueueConfig `mapstructure:"user_message_queue"`
- // Grok: Grok/xAI gateway scheduling and free-tier soft-gate settings.
- Grok GatewayGrokConfig `mapstructure:"grok"`
- // CNProviders: 国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)的余额检测配置。
- // 仅作用于 payg(按量付费)账号:周期探测余额,低于阈值则临时停调。
- CNProviders GatewayCNProvidersConfig `mapstructure:"cn_providers"`
- }
- // GatewayGrokConfig holds Grok-specific gateway scheduling knobs.
- //
- // Free-quota soft gate keys (gateway.grok.*):
- // - free_quota_soft_gate_enabled: enable local rolling-window scheduling guard for
- // OAuth accounts whose subscription_tier/plan_type is explicitly "free".
- // Default true is safe only because free-tier detection is strict (unknown/paid fail open).
- // - free_quota_token_limit: nominal rolling-window token allowance.
- // - free_quota_soft_gate_percent: stop new scheduling before the nominal limit (1-100).
- // - free_quota_window_hours: local usage rolling window length in hours.
- // - free_quota_stats_cache_seconds: cache TTL for free-tier usage stats
- // (hot path never blocks on DB; misses fail open and refresh in background).
- type GatewayGrokConfig struct {
- // PasswordAuthEnabled controls the optional password-to-SSO OAuth flow.
- // It defaults to false and must be explicitly enabled by the operator.
- // When true, POST /admin/grok/oauth/password is functional (not ignored).
- PasswordAuthEnabled bool `mapstructure:"password_auth_enabled"`
- // FreeQuotaSoftGateEnabled enables a local rolling-window scheduling guard
- // for explicitly free Grok OAuth accounts only.
- FreeQuotaSoftGateEnabled bool `mapstructure:"free_quota_soft_gate_enabled"`
- // FreeQuotaTokenLimit is the nominal rolling-window allowance.
- FreeQuotaTokenLimit int64 `mapstructure:"free_quota_token_limit"`
- // FreeQuotaSoftGatePercent stops new scheduling before the nominal limit.
- FreeQuotaSoftGatePercent int `mapstructure:"free_quota_soft_gate_percent"`
- // FreeQuotaWindowHours controls the local rolling usage window.
- FreeQuotaWindowHours int `mapstructure:"free_quota_window_hours"`
- // FreeQuotaStatsCacheSeconds is the soft-gate stats cache TTL. Hot path never
- // waits on usage_logs; misses fail open and refresh asynchronously.
- FreeQuotaStatsCacheSeconds int `mapstructure:"free_quota_stats_cache_seconds"`
- }
- // GatewayCNProvidersConfig 国产 OpenAI 兼容供应商(kimi/zhipu/deepseek)的余额检测配置。
- //
- // 仅作用于 payg(按量付费)账号(kimi/deepseek 有公开余额端点;zhipu 无,仅靠响应式 429/402)。
- // - balance_check_enabled: 是否启用周期余额检测(默认 true)
- // - balance_threshold: 余额低于此值(账户货币单位,默认 0.5)触发临时停调
- // - balance_check_interval_minutes: 余额检测周期(分钟,默认 10)
- type GatewayCNProvidersConfig struct {
- BalanceCheckEnabled bool `mapstructure:"balance_check_enabled"`
- BalanceThreshold float64 `mapstructure:"balance_threshold"`
- BalanceCheckIntervalMinutes int `mapstructure:"balance_check_interval_minutes"`
- }
- type GatewayLiveConfig struct {
- // MaxSessionDurationSeconds 是 Live 会话的硬上限。
- MaxSessionDurationSeconds int `mapstructure:"max_session_duration_seconds"`
- }
- // GatewayOpenAIHTTP2Config OpenAI HTTP 上游协议配置。
- // 默认启用 HTTP/2;在部分代理不兼容时按策略回退 HTTP/1.1。
- type GatewayOpenAIHTTP2Config struct {
- // Enabled: 是否启用 OpenAI HTTP/2 优先策略
- Enabled bool `mapstructure:"enabled"`
- // AllowProxyFallbackToHTTP1: HTTP/HTTPS 代理出现明确 H2 兼容错误时,临时回退 HTTP/1.1
- AllowProxyFallbackToHTTP1 bool `mapstructure:"allow_proxy_fallback_to_http1"`
- // FallbackErrorThreshold: 回退窗口内累计多少次兼容错误后触发回退
- FallbackErrorThreshold int `mapstructure:"fallback_error_threshold"`
- // FallbackWindowSeconds: 统计兼容错误的时间窗口(秒)
- FallbackWindowSeconds int `mapstructure:"fallback_window_seconds"`
- // FallbackTTLSeconds: 触发后回退 HTTP/1.1 的持续时间(秒)
- FallbackTTLSeconds int `mapstructure:"fallback_ttl_seconds"`
- }
- // GatewayOpenAIProxyStreamCircuitConfig controls the bounded, in-process
- // proxy-ID circuit used for incomplete OpenAI Responses SSE streams.
- type GatewayOpenAIProxyStreamCircuitConfig struct {
- // Disabled: 完全关闭代理断流熔断(默认开启)。
- Disabled bool `mapstructure:"disabled"`
- // FailureThreshold: 统计窗口内多少次断流后隔离代理。
- FailureThreshold int `mapstructure:"failure_threshold"`
- // WindowSeconds: 断流统计窗口(秒)。
- WindowSeconds int `mapstructure:"window_seconds"`
- // TTLSeconds: 代理隔离持续时间(秒)。
- TTLSeconds int `mapstructure:"ttl_seconds"`
- }
- // UserMessageQueueConfig 用户消息串行队列配置
- // 用于 Anthropic OAuth/SetupToken 账号的用户消息串行化发送
- type UserMessageQueueConfig struct {
- // Mode: 模式选择
- // "serialize" = 账号级串行锁 + RPM 自适应延迟
- // "throttle" = 仅 RPM 自适应前置延迟,不阻塞并发
- // "" = 禁用(默认)
- Mode string `mapstructure:"mode"`
- // Enabled: 已废弃,仅向后兼容(等同于 mode: "serialize")
- Enabled bool `mapstructure:"enabled"`
- // LockTTLMs: 串行锁 TTL(毫秒),应大于最长请求时间
- LockTTLMs int `mapstructure:"lock_ttl_ms"`
- // WaitTimeoutMs: 等待获取锁的超时时间(毫秒)
- WaitTimeoutMs int `mapstructure:"wait_timeout_ms"`
- // MinDelayMs: RPM 自适应延迟下限(毫秒)
- MinDelayMs int `mapstructure:"min_delay_ms"`
- // MaxDelayMs: RPM 自适应延迟上限(毫秒)
- MaxDelayMs int `mapstructure:"max_delay_ms"`
- // CleanupIntervalSeconds: 孤儿锁清理间隔(秒),0 表示禁用
- CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"`
- }
- // WaitTimeout 返回等待超时的 time.Duration
- func (c *UserMessageQueueConfig) WaitTimeout() time.Duration {
- if c.WaitTimeoutMs <= 0 {
- return 30 * time.Second
- }
- return time.Duration(c.WaitTimeoutMs) * time.Millisecond
- }
- // GetEffectiveMode 返回生效的模式
- // 注意:Mode 字段已在 load() 中做过白名单校验和规范化,此处无需重复验证
- func (c *UserMessageQueueConfig) GetEffectiveMode() string {
- if c.Mode == UMQModeSerialize || c.Mode == UMQModeThrottle {
- return c.Mode
- }
- if c.Enabled {
- return UMQModeSerialize // 向后兼容
- }
- return ""
- }
- // DefaultOpenAIWSClientFirstMessageTimeoutSeconds preserves the legacy ingress deadline.
- const DefaultOpenAIWSClientFirstMessageTimeoutSeconds = 30
- // GatewayOpenAIWSConfig OpenAI Responses WebSocket 配置。
- // 注意:默认全局开启;如需回滚可使用 force_http 或关闭 enabled。
- type GatewayOpenAIWSConfig struct {
- // ModeRouterV2Enabled: 新版 WS mode 路由开关(默认 false;关闭时保持 legacy 行为)
- ModeRouterV2Enabled bool `mapstructure:"mode_router_v2_enabled"`
- // IngressModeDefault: ingress 默认模式(off/ctx_pool/passthrough/http_bridge)
- IngressModeDefault string `mapstructure:"ingress_mode_default"`
- // ClientFirstMessageTimeoutSeconds bounds the total time to read and decompress
- // the first client response.create message after the WebSocket upgrade.
- ClientFirstMessageTimeoutSeconds int `mapstructure:"client_first_message_timeout_seconds"`
- // IngressInterTurnIdleTimeoutSeconds bounds the time a client may remain idle
- // between completed ingress turns. Zero disables this protection.
- IngressInterTurnIdleTimeoutSeconds int `mapstructure:"ingress_inter_turn_idle_timeout_seconds"`
- // MaxIngressConnectionsPerAPIKey bounds live client WebSocket ingress sessions
- // per API key across all instances. Zero disables this protection.
- MaxIngressConnectionsPerAPIKey int `mapstructure:"max_ingress_connections_per_api_key"`
- // Enabled: 全局总开关(默认 true)
- Enabled bool `mapstructure:"enabled"`
- // OAuthEnabled: 是否允许 OpenAI OAuth 账号使用 WS
- OAuthEnabled bool `mapstructure:"oauth_enabled"`
- // APIKeyEnabled: 是否允许 OpenAI API Key 账号使用 WS
- APIKeyEnabled bool `mapstructure:"apikey_enabled"`
- // ForceHTTP: 全局强制 HTTP(用于紧急回滚)
- ForceHTTP bool `mapstructure:"force_http"`
- // AllowStoreRecovery: 允许在 WSv2 下按策略恢复 store=true(默认 false)
- AllowStoreRecovery bool `mapstructure:"allow_store_recovery"`
- // IngressPreviousResponseRecoveryEnabled: ingress 模式收到 previous_response_not_found 时,是否允许自动去掉 previous_response_id 重试一次(默认 true)
- IngressPreviousResponseRecoveryEnabled bool `mapstructure:"ingress_previous_response_recovery_enabled"`
- // StoreDisabledConnMode: store=false 且无可复用会话连接时的建连策略(strict/adaptive/off)
- // - strict: 强制新建连接(隔离优先)
- // - adaptive: 仅在高风险失败后强制新建连接(性能与隔离折中)
- // - off: 不强制新建连接(复用优先)
- StoreDisabledConnMode string `mapstructure:"store_disabled_conn_mode"`
- // StoreDisabledForceNewConn: store=false 且无可复用粘连连接时是否强制新建连接(默认 true,保障会话隔离)
- // 兼容旧配置;当 StoreDisabledConnMode 为空时才生效。
- StoreDisabledForceNewConn bool `mapstructure:"store_disabled_force_new_conn"`
- // PrewarmGenerateEnabled: 是否启用 WSv2 generate=false 预热(默认 false)
- PrewarmGenerateEnabled bool `mapstructure:"prewarm_generate_enabled"`
- // ClientReadLimitBytes: 入站客户端 WS 单帧读取上限。
- ClientReadLimitBytes int64 `mapstructure:"client_read_limit_bytes"`
- // HTTPBridgeEnabled: 首包过大时,保持客户端 WS,改用 HTTP Responses 上游。
- HTTPBridgeEnabled bool `mapstructure:"http_bridge_enabled"`
- // HTTPBridgeThresholdBytes: 触发 HTTP bridge 的入站 WS payload 阈值。
- HTTPBridgeThresholdBytes int64 `mapstructure:"http_bridge_threshold_bytes"`
- // Feature 开关:v2 优先于 v1
- ResponsesWebsockets bool `mapstructure:"responses_websockets"`
- ResponsesWebsocketsV2 bool `mapstructure:"responses_websockets_v2"`
- // 连接池参数
- MaxConnsPerAccount int `mapstructure:"max_conns_per_account"`
- MinIdlePerAccount int `mapstructure:"min_idle_per_account"`
- MaxIdlePerAccount int `mapstructure:"max_idle_per_account"`
- // DynamicMaxConnsByAccountConcurrencyEnabled: 是否按账号并发动态计算连接池上限
- DynamicMaxConnsByAccountConcurrencyEnabled bool `mapstructure:"dynamic_max_conns_by_account_concurrency_enabled"`
- // OAuthMaxConnsFactor: OAuth 账号连接池系数(effective=ceil(concurrency*factor))
- OAuthMaxConnsFactor float64 `mapstructure:"oauth_max_conns_factor"`
- // APIKeyMaxConnsFactor: API Key 账号连接池系数(effective=ceil(concurrency*factor))
- APIKeyMaxConnsFactor float64 `mapstructure:"apikey_max_conns_factor"`
- DialTimeoutSeconds int `mapstructure:"dial_timeout_seconds"`
- ReadTimeoutSeconds int `mapstructure:"read_timeout_seconds"`
- WriteTimeoutSeconds int `mapstructure:"write_timeout_seconds"`
- PoolTargetUtilization float64 `mapstructure:"pool_target_utilization"`
- QueueLimitPerConn int `mapstructure:"queue_limit_per_conn"`
- // EventFlushBatchSize: WS 流式写出批量 flush 阈值(事件条数)
- EventFlushBatchSize int `mapstructure:"event_flush_batch_size"`
- // EventFlushIntervalMS: WS 流式写出最大等待时间(毫秒);0 表示仅按 batch 触发
- EventFlushIntervalMS int `mapstructure:"event_flush_interval_ms"`
- // PrewarmCooldownMS: 连接池预热触发冷却时间(毫秒)
- PrewarmCooldownMS int `mapstructure:"prewarm_cooldown_ms"`
- // FallbackCooldownSeconds: WS 回退冷却窗口,避免 WS/HTTP 抖动;0 表示关闭冷却
- FallbackCooldownSeconds int `mapstructure:"fallback_cooldown_seconds"`
- // RetryBackoffInitialMS: WS 重试初始退避(毫秒);<=0 表示关闭退避
- RetryBackoffInitialMS int `mapstructure:"retry_backoff_initial_ms"`
- // RetryBackoffMaxMS: WS 重试最大退避(毫秒)
- RetryBackoffMaxMS int `mapstructure:"retry_backoff_max_ms"`
- // RetryJitterRatio: WS 重试退避抖动比例(0-1)
- RetryJitterRatio float64 `mapstructure:"retry_jitter_ratio"`
- // RetryTotalBudgetMS: WS 单次请求重试总预算(毫秒);0 表示关闭预算限制
- RetryTotalBudgetMS int `mapstructure:"retry_total_budget_ms"`
- // PayloadLogSampleRate: payload_schema 日志采样率(0-1)
- PayloadLogSampleRate float64 `mapstructure:"payload_log_sample_rate"`
- // 账号调度与粘连参数
- LBTopK int `mapstructure:"lb_top_k"`
- // StickySessionTTLSeconds: session_hash -> account_id 粘连 TTL
- StickySessionTTLSeconds int `mapstructure:"sticky_session_ttl_seconds"`
- // SessionHashReadOldFallback: 会话哈希迁移期是否允许“新 key 未命中时回退读旧 SHA-256 key”
- SessionHashReadOldFallback bool `mapstructure:"session_hash_read_old_fallback"`
- // SessionHashDualWriteOld: 会话哈希迁移期是否双写旧 SHA-256 key(短 TTL)
- SessionHashDualWriteOld bool `mapstructure:"session_hash_dual_write_old"`
- // MetadataBridgeEnabled: RequestMetadata 迁移期是否保留旧 ctxkey.* 兼容桥接
- MetadataBridgeEnabled bool `mapstructure:"metadata_bridge_enabled"`
- // StickyResponseIDTTLSeconds: response_id -> account_id 粘连 TTL
- StickyResponseIDTTLSeconds int `mapstructure:"sticky_response_id_ttl_seconds"`
- // StickyPreviousResponseTTLSeconds: 兼容旧键(当新键未设置时回退)
- StickyPreviousResponseTTLSeconds int `mapstructure:"sticky_previous_response_ttl_seconds"`
- SchedulerScoreWeights GatewayOpenAIWSSchedulerScoreWeights `mapstructure:"scheduler_score_weights"`
- }
- // GatewayOpenAIWSSchedulerScoreWeights 账号调度打分权重。
- type GatewayOpenAIWSSchedulerScoreWeights struct {
- Priority float64 `mapstructure:"priority"`
- Load float64 `mapstructure:"load"`
- Queue float64 `mapstructure:"queue"`
- ErrorRate float64 `mapstructure:"error_rate"`
- TTFT float64 `mapstructure:"ttft"`
- // Reset 倾向「会话窗口最早重置」的账号(use-it-or-lose-it)。
- // >0 时,剩余重置时间越短的账号得分越高,从而被优先用尽。默认 0(关闭,不改变原有行为)。
- Reset float64 `mapstructure:"reset"`
- // QuotaHeadroom 倾向 7d 剩余额度更健康的账号;默认 0(关闭,不改变原有行为)。
- QuotaHeadroom float64 `mapstructure:"quota_headroom"`
- // UpstreamCost 倾向上游声明倍率更低的账号;默认 0(关闭,不改变原有行为)。
- UpstreamCost float64 `mapstructure:"upstream_cost"`
- // PreviousResponse/SessionSticky 仅在开启 OpenAI 高级调度的粘性加权时生效。
- PreviousResponse float64 `mapstructure:"previous_response"`
- SessionSticky float64 `mapstructure:"session_sticky"`
- }
- func (w GatewayOpenAIWSSchedulerScoreWeights) BaseWeightSum() float64 {
- return w.Priority + w.Load + w.Queue + w.ErrorRate + w.TTFT + w.Reset + w.QuotaHeadroom + w.UpstreamCost
- }
- func (w GatewayOpenAIWSSchedulerScoreWeights) TotalWeightSum() float64 {
- return w.BaseWeightSum() + w.PreviousResponse + w.SessionSticky
- }
- func (w GatewayOpenAIWSSchedulerScoreWeights) IsValid() bool {
- for _, weight := range []float64{
- w.Priority, w.Load, w.Queue, w.ErrorRate, w.TTFT, w.Reset,
- w.QuotaHeadroom, w.UpstreamCost, w.PreviousResponse, w.SessionSticky,
- } {
- if weight < 0 || math.IsNaN(weight) || math.IsInf(weight, 0) {
- return false
- }
- }
- baseSum := w.BaseWeightSum()
- return baseSum > 0 && !math.IsNaN(baseSum) && !math.IsInf(baseSum, 0) &&
- !math.IsNaN(w.TotalWeightSum()) && !math.IsInf(w.TotalWeightSum(), 0)
- }
- // GatewayOpenAISchedulerConfig OpenAI 高级调度器配置。
- type GatewayOpenAISchedulerConfig struct {
- // StickyEscapeEnabled: 是否允许 session_hash sticky 在账号健康度劣化时临时逃逸
- StickyEscapeEnabled bool `mapstructure:"sticky_escape_enabled"`
- // StickyEscapeTTFTMs: TTFT EWMA 超过该阈值时跳过 sticky
- StickyEscapeTTFTMs int `mapstructure:"sticky_escape_ttft_ms"`
- // StickyEscapeErrorRate: 错误率 EWMA 超过该阈值时跳过 sticky
- StickyEscapeErrorRate float64 `mapstructure:"sticky_escape_error_rate"`
- }
- // GatewayUsageRecordConfig 使用量记录异步队列配置
- type GatewayUsageRecordConfig struct {
- // WorkerCount: worker 初始数量(自动扩缩容开启时作为初始并发上限)
- WorkerCount int `mapstructure:"worker_count"`
- // QueueSize: 队列容量(有界)
- QueueSize int `mapstructure:"queue_size"`
- // TaskTimeoutSeconds: 单个使用量记录任务超时(秒)
- TaskTimeoutSeconds int `mapstructure:"task_timeout_seconds"`
- // OverflowPolicy: 队列满时策略(drop/sample/sync)
- OverflowPolicy string `mapstructure:"overflow_policy"`
- // OverflowSamplePercent: sample 策略下,同步回写采样百分比(1-100)
- OverflowSamplePercent int `mapstructure:"overflow_sample_percent"`
- // AutoScaleEnabled: 是否启用 worker 自动扩缩容
- AutoScaleEnabled bool `mapstructure:"auto_scale_enabled"`
- // AutoScaleMinWorkers: 自动扩缩容最小 worker 数
- AutoScaleMinWorkers int `mapstructure:"auto_scale_min_workers"`
- // AutoScaleMaxWorkers: 自动扩缩容最大 worker 数
- AutoScaleMaxWorkers int `mapstructure:"auto_scale_max_workers"`
- // AutoScaleUpQueuePercent: 队列占用率达到该阈值时触发扩容
- AutoScaleUpQueuePercent int `mapstructure:"auto_scale_up_queue_percent"`
- // AutoScaleDownQueuePercent: 队列占用率低于该阈值时触发缩容
- AutoScaleDownQueuePercent int `mapstructure:"auto_scale_down_queue_percent"`
- // AutoScaleUpStep: 每次扩容步长
- AutoScaleUpStep int `mapstructure:"auto_scale_up_step"`
- // AutoScaleDownStep: 每次缩容步长
- AutoScaleDownStep int `mapstructure:"auto_scale_down_step"`
- // AutoScaleCheckIntervalSeconds: 自动扩缩容检测间隔(秒)
- AutoScaleCheckIntervalSeconds int `mapstructure:"auto_scale_check_interval_seconds"`
- // AutoScaleCooldownSeconds: 自动扩缩容冷却时间(秒)
- AutoScaleCooldownSeconds int `mapstructure:"auto_scale_cooldown_seconds"`
- }
- // TLSFingerprintConfig TLS指纹伪装配置
- // 用于模拟 Claude CLI (Node.js) 的 TLS 握手特征,避免被识别为非官方客户端
- type TLSFingerprintConfig struct {
- // Enabled: 是否全局启用TLS指纹功能
- Enabled bool `mapstructure:"enabled"`
- // Profiles: 预定义的TLS指纹配置模板
- // key 为模板名称,如 "claude_cli_v2", "chrome_120" 等
- Profiles map[string]TLSProfileConfig `mapstructure:"profiles"`
- }
- // TLSProfileConfig 单个TLS指纹模板的配置
- // 所有列表字段为空时使用内置默认值(Claude CLI 2.x / Node.js 20.x)
- // 建议通过 TLS 指纹采集工具 (tests/tls-fingerprint-web) 获取完整配置
- type TLSProfileConfig struct {
- // Name: 模板显示名称
- Name string `mapstructure:"name"`
- // EnableGREASE: 是否启用GREASE扩展(Chrome使用,Node.js不使用)
- EnableGREASE bool `mapstructure:"enable_grease"`
- // CipherSuites: TLS加密套件列表
- CipherSuites []uint16 `mapstructure:"cipher_suites"`
- // Curves: 椭圆曲线列表
- Curves []uint16 `mapstructure:"curves"`
- // PointFormats: 点格式列表
- PointFormats []uint16 `mapstructure:"point_formats"`
- // SignatureAlgorithms: 签名算法列表
- SignatureAlgorithms []uint16 `mapstructure:"signature_algorithms"`
- // ALPNProtocols: ALPN协议列表(如 ["h2", "http/1.1"])
- ALPNProtocols []string `mapstructure:"alpn_protocols"`
- // SupportedVersions: 支持的TLS版本列表(如 [0x0304, 0x0303] 即 TLS1.3, TLS1.2)
- SupportedVersions []uint16 `mapstructure:"supported_versions"`
- // KeyShareGroups: Key Share中发送的曲线组(如 [29] 即 X25519)
- KeyShareGroups []uint16 `mapstructure:"key_share_groups"`
- // PSKModes: PSK密钥交换模式(如 [1] 即 psk_dhe_ke)
- PSKModes []uint16 `mapstructure:"psk_modes"`
- // Extensions: TLS扩展类型ID列表,按发送顺序排列
- // 空则使用内置默认顺序 [0,11,10,35,16,22,23,13,43,45,51]
- // GREASE值(如0x0a0a)会自动插入GREASE扩展
- Extensions []uint16 `mapstructure:"extensions"`
- }
- // GatewaySchedulingConfig accounts scheduling configuration.
- type GatewaySchedulingConfig struct {
- // 粘性会话排队配置
- StickySessionMaxWaiting int `mapstructure:"sticky_session_max_waiting"`
- StickySessionWaitTimeout time.Duration `mapstructure:"sticky_session_wait_timeout"`
- // 兜底排队配置
- FallbackWaitTimeout time.Duration `mapstructure:"fallback_wait_timeout"`
- FallbackMaxWaiting int `mapstructure:"fallback_max_waiting"`
- // 兜底层账户选择策略: "last_used"(按最后使用时间排序,默认) 或 "random"(随机)
- FallbackSelectionMode string `mapstructure:"fallback_selection_mode"`
- // PreferSoonestReset 开启后,负载感知选择会优先选用「会话窗口最早重置」的账号
- // (use-it-or-lose-it:先用尽即将重置的账号,保留重置时间还很久的账号)。
- // 默认 false,保持原有「优先级 → 负载率 → LRU」行为不变。
- PreferSoonestReset bool `mapstructure:"prefer_soonest_reset"`
- // 负载计算
- LoadBatchEnabled bool `mapstructure:"load_batch_enabled"`
- LoadBatchCacheTTLMS int `mapstructure:"load_batch_cache_ttl_ms"`
- // 快照桶读取时的 MGET 分块大小
- SnapshotMGetChunkSize int `mapstructure:"snapshot_mget_chunk_size"`
- // 快照重建时的缓存写入分块大小
- SnapshotWriteChunkSize int `mapstructure:"snapshot_write_chunk_size"`
- // 过期槽位清理周期(0 表示禁用)
- SlotCleanupInterval time.Duration `mapstructure:"slot_cleanup_interval"`
- // 受控回源配置
- DbFallbackEnabled bool `mapstructure:"db_fallback_enabled"`
- // 受控回源超时(秒),0 表示不额外收紧超时
- DbFallbackTimeoutSeconds int `mapstructure:"db_fallback_timeout_seconds"`
- // 受控回源限流(实例级 QPS),0 表示不限制
- DbFallbackMaxQPS int `mapstructure:"db_fallback_max_qps"`
- // Outbox 轮询与滞后阈值配置
- // Outbox 轮询周期(秒)
- OutboxPollIntervalSeconds int `mapstructure:"outbox_poll_interval_seconds"`
- // Outbox 滞后告警阈值(秒)
- OutboxLagWarnSeconds int `mapstructure:"outbox_lag_warn_seconds"`
- // Outbox 触发强制重建阈值(秒)
- OutboxLagRebuildSeconds int `mapstructure:"outbox_lag_rebuild_seconds"`
- // Outbox 连续滞后触发次数
- OutboxLagRebuildFailures int `mapstructure:"outbox_lag_rebuild_failures"`
- // Outbox 积压触发重建阈值(行数)
- OutboxBacklogRebuildRows int `mapstructure:"outbox_backlog_rebuild_rows"`
- // 全量重建周期配置
- // 全量重建周期(秒),0 表示禁用
- FullRebuildIntervalSeconds int `mapstructure:"full_rebuild_interval_seconds"`
- }
- func (s *ServerConfig) Address() string {
- return fmt.Sprintf("%s:%d", s.Host, s.Port)
- }
- // DatabaseConfig 数据库连接配置
- // 性能优化:新增连接池参数,避免频繁创建/销毁连接
- type DatabaseConfig struct {
- Host string `mapstructure:"host"`
- Port int `mapstructure:"port"`
- User string `mapstructure:"user"`
- Password string `mapstructure:"password"`
- DBName string `mapstructure:"dbname"`
- SSLMode string `mapstructure:"sslmode"`
- // 连接池配置(性能优化:可配置化连接池参数)
- // MaxOpenConns: 最大打开连接数,控制数据库连接上限,防止资源耗尽
- MaxOpenConns int `mapstructure:"max_open_conns"`
- // MaxIdleConns: 最大空闲连接数,保持热连接减少建连延迟
- MaxIdleConns int `mapstructure:"max_idle_conns"`
- // ConnMaxLifetimeMinutes: 连接最大存活时间,防止长连接导致的资源泄漏
- ConnMaxLifetimeMinutes int `mapstructure:"conn_max_lifetime_minutes"`
- // ConnMaxIdleTimeMinutes: 空闲连接最大存活时间,及时释放不活跃连接
- ConnMaxIdleTimeMinutes int `mapstructure:"conn_max_idle_time_minutes"`
- // UserPlatformQuotaFlusherEnabled: 是否启用 user×platform 配额写聚合 flusher
- UserPlatformQuotaFlusherEnabled bool `mapstructure:"user_platform_quota_flusher_enabled"`
- // UserPlatformQuotaFlushIntervalMs: flusher 刷写间隔(毫秒)
- UserPlatformQuotaFlushIntervalMs int `mapstructure:"user_platform_quota_flush_interval_ms"`
- // UserPlatformQuotaFlushBatchSize: flusher 单批最大条数
- // 建议 ≤ 6000(单条 UPSERT 原子上限)
- UserPlatformQuotaFlushBatchSize int `mapstructure:"user_platform_quota_flush_batch_size"`
- }
- func (d *DatabaseConfig) DSN() string {
- // 当密码为空时不包含 password 参数,避免 libpq 解析错误
- if d.Password == "" {
- return fmt.Sprintf(
- "host=%s port=%d user=%s dbname=%s sslmode=%s",
- d.Host, d.Port, d.User, d.DBName, d.SSLMode,
- )
- }
- return fmt.Sprintf(
- "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
- d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode,
- )
- }
- // DSNWithTimezone returns DSN with timezone setting
- func (d *DatabaseConfig) DSNWithTimezone(tz string) string {
- if tz == "" {
- tz = "Asia/Shanghai"
- }
- // 当密码为空时不包含 password 参数,避免 libpq 解析错误
- if d.Password == "" {
- return fmt.Sprintf(
- "host=%s port=%d user=%s dbname=%s sslmode=%s TimeZone=%s",
- d.Host, d.Port, d.User, d.DBName, d.SSLMode, tz,
- )
- }
- return fmt.Sprintf(
- "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
- d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode, tz,
- )
- }
- // RedisConfig Redis 连接配置
- // 性能优化:新增连接池和超时参数,提升高并发场景下的吞吐量
- type RedisConfig struct {
- Host string `mapstructure:"host"`
- Port int `mapstructure:"port"`
- Username string `mapstructure:"username"`
- Password string `mapstructure:"password"`
- DB int `mapstructure:"db"`
- // 连接池与超时配置(性能优化:可配置化连接池参数)
- // DialTimeoutSeconds: 建立连接超时,防止慢连接阻塞
- DialTimeoutSeconds int `mapstructure:"dial_timeout_seconds"`
- // ReadTimeoutSeconds: 读取超时,避免慢查询阻塞连接池
- ReadTimeoutSeconds int `mapstructure:"read_timeout_seconds"`
- // WriteTimeoutSeconds: 写入超时,避免慢写入阻塞连接池
- WriteTimeoutSeconds int `mapstructure:"write_timeout_seconds"`
- // PoolSize: 连接池大小,控制最大并发连接数
- PoolSize int `mapstructure:"pool_size"`
- // MinIdleConns: 最小空闲连接数,保持热连接减少冷启动延迟
- MinIdleConns int `mapstructure:"min_idle_conns"`
- // EnableTLS: 是否启用 TLS/SSL 连接
- EnableTLS bool `mapstructure:"enable_tls"`
- }
- func (r *RedisConfig) Address() string {
- return fmt.Sprintf("%s:%d", r.Host, r.Port)
- }
- type OpsConfig struct {
- // Enabled controls whether ops features should run.
- //
- // NOTE: vNext still has a DB-backed feature flag (ops_monitoring_enabled) for runtime on/off.
- // This config flag is the "hard switch" for deployments that want to disable ops completely.
- Enabled bool `mapstructure:"enabled"`
- // UsePreaggregatedTables prefers ops_metrics_hourly/daily for long-window dashboard queries.
- UsePreaggregatedTables bool `mapstructure:"use_preaggregated_tables"`
- // Cleanup controls periodic deletion of old ops data to prevent unbounded growth.
- Cleanup OpsCleanupConfig `mapstructure:"cleanup"`
- // MetricsCollectorCache controls Redis caching for expensive per-window collector queries.
- MetricsCollectorCache OpsMetricsCollectorCacheConfig `mapstructure:"metrics_collector_cache"`
- // Pre-aggregation configuration.
- Aggregation OpsAggregationConfig `mapstructure:"aggregation"`
- }
- type OpsCleanupConfig struct {
- Enabled bool `mapstructure:"enabled"`
- Schedule string `mapstructure:"schedule"`
- // Retention days. Error and metrics targets accept 0 as an explicit truncate;
- // system logs require a positive value because their runtime setting is bounded.
- ErrorLogRetentionDays int `mapstructure:"error_log_retention_days"`
- SystemLogRetentionDays int `mapstructure:"system_log_retention_days"`
- MinuteMetricsRetentionDays int `mapstructure:"minute_metrics_retention_days"`
- HourlyMetricsRetentionDays int `mapstructure:"hourly_metrics_retention_days"`
- }
- type OpsAggregationConfig struct {
- Enabled bool `mapstructure:"enabled"`
- }
- type OpsMetricsCollectorCacheConfig struct {
- Enabled bool `mapstructure:"enabled"`
- TTL time.Duration `mapstructure:"ttl"`
- }
- type JWTConfig struct {
- Secret string `mapstructure:"secret"`
- ExpireHour int `mapstructure:"expire_hour"`
- // AccessTokenExpireMinutes: Access Token有效期(分钟)
- // - >0: 使用分钟配置(优先级高于 ExpireHour)
- // - =0: 回退使用 ExpireHour(向后兼容旧配置)
- AccessTokenExpireMinutes int `mapstructure:"access_token_expire_minutes"`
- // RefreshTokenExpireDays: Refresh Token有效期(天),默认30天
- RefreshTokenExpireDays int `mapstructure:"refresh_token_expire_days"`
- // RefreshWindowMinutes: 刷新窗口(分钟),在Access Token过期前多久开始允许刷新
- RefreshWindowMinutes int `mapstructure:"refresh_window_minutes"`
- }
- // TotpConfig TOTP 双因素认证配置
- type TotpConfig struct {
- // EncryptionKey 用于加密 TOTP 密钥的 AES-256 密钥(32 字节 hex 编码)
- // 如果为空,将自动生成一个随机密钥(仅适用于开发环境)
- EncryptionKey string `mapstructure:"encryption_key"`
- // EncryptionKeyConfigured 标记加密密钥是否为手动配置(非自动生成)
- // 只有手动配置了密钥才允许在管理后台启用 TOTP 功能
- EncryptionKeyConfigured bool `mapstructure:"-"`
- }
- type TurnstileConfig struct {
- Required bool `mapstructure:"required"`
- }
- type DefaultConfig struct {
- AdminEmail string `mapstructure:"admin_email"`
- AdminPassword string `mapstructure:"admin_password"`
- UserConcurrency int `mapstructure:"user_concurrency"`
- UserBalance float64 `mapstructure:"user_balance"`
- APIKeyPrefix string `mapstructure:"api_key_prefix"`
- RateMultiplier float64 `mapstructure:"rate_multiplier"`
- }
- type RateLimitConfig struct {
- OverloadCooldownMinutes int `mapstructure:"overload_cooldown_minutes"` // 529过载冷却时间(分钟)
- OAuth401CooldownMinutes int `mapstructure:"oauth_401_cooldown_minutes"` // OAuth 401临时不可调度冷却(分钟)
- }
- // APIKeyAuthCacheConfig API Key 认证缓存配置
- type APIKeyAuthCacheConfig struct {
- L1Size int `mapstructure:"l1_size"`
- L1TTLSeconds int `mapstructure:"l1_ttl_seconds"`
- L2TTLSeconds int `mapstructure:"l2_ttl_seconds"`
- NegativeTTLSeconds int `mapstructure:"negative_ttl_seconds"`
- JitterPercent int `mapstructure:"jitter_percent"`
- Singleflight bool `mapstructure:"singleflight"`
- LookupConcurrency int `mapstructure:"lookup_concurrency"`
- InvalidAbuse InvalidAuthAbuseConfig `mapstructure:"invalid_abuse"`
- }
- type InvalidAuthAbuseConfig struct {
- Enabled bool `mapstructure:"enabled"`
- Threshold int `mapstructure:"threshold"`
- WindowSeconds int `mapstructure:"window_seconds"`
- BlockSeconds int `mapstructure:"block_seconds"`
- Capacity int `mapstructure:"capacity"`
- }
- // SubscriptionCacheConfig 订阅认证 L1 缓存配置
- type SubscriptionCacheConfig struct {
- L1Size int `mapstructure:"l1_size"`
- L1TTLSeconds int `mapstructure:"l1_ttl_seconds"`
- JitterPercent int `mapstructure:"jitter_percent"`
- }
- // SubscriptionMaintenanceConfig 订阅窗口维护后台任务配置。
- // 用于将“请求路径触发的维护动作”有界化,避免高并发下 goroutine 膨胀。
- type SubscriptionMaintenanceConfig struct {
- WorkerCount int `mapstructure:"worker_count"`
- QueueSize int `mapstructure:"queue_size"`
- }
- // DashboardCacheConfig 仪表盘统计缓存配置
- type DashboardCacheConfig struct {
- // Enabled: 是否启用仪表盘缓存
- Enabled bool `mapstructure:"enabled"`
- // KeyPrefix: Redis key 前缀,用于多环境隔离
- KeyPrefix string `mapstructure:"key_prefix"`
- // StatsFreshTTLSeconds: 缓存命中认为“新鲜”的时间窗口(秒)
- StatsFreshTTLSeconds int `mapstructure:"stats_fresh_ttl_seconds"`
- // StatsTTLSeconds: Redis 缓存总 TTL(秒)
- StatsTTLSeconds int `mapstructure:"stats_ttl_seconds"`
- // StatsRefreshTimeoutSeconds: 异步刷新超时(秒)
- StatsRefreshTimeoutSeconds int `mapstructure:"stats_refresh_timeout_seconds"`
- }
- // DashboardAggregationConfig 仪表盘预聚合配置
- type DashboardAggregationConfig struct {
- // Enabled: 是否启用预聚合作业
- Enabled bool `mapstructure:"enabled"`
- // IntervalSeconds: 聚合刷新间隔(秒)
- IntervalSeconds int `mapstructure:"interval_seconds"`
- // LookbackSeconds: 回看窗口(秒)
- LookbackSeconds int `mapstructure:"lookback_seconds"`
- // BackfillEnabled: 是否允许全量回填
- BackfillEnabled bool `mapstructure:"backfill_enabled"`
- // BackfillMaxDays: 回填最大跨度(天)
- BackfillMaxDays int `mapstructure:"backfill_max_days"`
- // Retention: 各表保留窗口(天)
- Retention DashboardAggregationRetentionConfig `mapstructure:"retention"`
- // RecomputeDays: 启动时重算最近 N 天
- RecomputeDays int `mapstructure:"recompute_days"`
- }
- // DashboardAggregationRetentionConfig 预聚合保留窗口
- type DashboardAggregationRetentionConfig struct {
- UsageLogsDays int `mapstructure:"usage_logs_days"`
- UsageBillingDedupDays int `mapstructure:"usage_billing_dedup_days"`
- HourlyDays int `mapstructure:"hourly_days"`
- DailyDays int `mapstructure:"daily_days"`
- }
- // UsageCleanupConfig 使用记录清理任务配置
- type UsageCleanupConfig struct {
- // Enabled: 是否启用清理任务执行器
- Enabled bool `mapstructure:"enabled"`
- // MaxRangeDays: 单次任务允许的最大时间跨度(天)
- MaxRangeDays int `mapstructure:"max_range_days"`
- // BatchSize: 单批删除数量
- BatchSize int `mapstructure:"batch_size"`
- // WorkerIntervalSeconds: 后台任务轮询间隔(秒)
- WorkerIntervalSeconds int `mapstructure:"worker_interval_seconds"`
- // TaskTimeoutSeconds: 单次任务最大执行时长(秒)
- TaskTimeoutSeconds int `mapstructure:"task_timeout_seconds"`
- }
- func NormalizeRunMode(value string) string {
- normalized := strings.ToLower(strings.TrimSpace(value))
- switch normalized {
- case RunModeStandard, RunModeSimple:
- return normalized
- default:
- return RunModeStandard
- }
- }
- // Load 读取并校验完整配置(要求 jwt.secret 已显式提供)。
- func Load() (*Config, error) {
- return load(false)
- }
- // LoadForBootstrap 读取启动阶段配置。
- //
- // 启动阶段允许 jwt.secret 先留空,后续由数据库初始化流程补齐并再次完整校验。
- func LoadForBootstrap() (*Config, error) {
- return load(true)
- }
- func load(allowMissingJWTSecret bool) (*Config, error) {
- viper.SetConfigName("config")
- viper.SetConfigType("yaml")
- configureConfigSource(viper.SetConfigFile, viper.AddConfigPath)
- // 环境变量支持
- viper.AutomaticEnv()
- viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
- if tz, ok := os.LookupEnv("TZ"); ok && strings.TrimSpace(tz) != "" {
- // AutomaticEnv 会先把 timezone 映射到 TIMEZONE;显式 Set 保证标准 TZ 变量优先。
- viper.Set("timezone", strings.TrimSpace(tz))
- }
- if err := viper.BindEnv("server.enable_server_timing", "ENABLE_SERVER_TIMING"); err != nil {
- return nil, fmt.Errorf("bind ENABLE_SERVER_TIMING: %w", err)
- }
- // 默认值
- setDefaults()
- if err := viper.ReadInConfig(); err != nil {
- if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
- return nil, fmt.Errorf("read config error: %w", err)
- }
- // 配置文件不存在时使用默认值
- }
- trustedProxiesEnv, trustedProxiesEnvConfigured := os.LookupEnv("SERVER_TRUSTED_PROXIES")
- forwardedClientIPHeadersEnv, forwardedClientIPHeadersEnvConfigured := os.LookupEnv("SECURITY_FORWARDED_CLIENT_IP_HEADERS")
- trustedProxiesConfigured := viper.InConfig("server.trusted_proxies") ||
- viper.IsSet("server.trusted_proxies") || trustedProxiesEnvConfigured
- var cfg Config
- if err := viper.Unmarshal(&cfg); err != nil {
- return nil, fmt.Errorf("unmarshal config error: %w", err)
- }
- if trustedProxiesEnvConfigured {
- cfg.Server.TrustedProxies = normalizeStringSlice(strings.Split(trustedProxiesEnv, ","))
- }
- if forwardedClientIPHeadersEnvConfigured {
- cfg.Security.ForwardedClientIPHeaders = normalizeStringSlice(strings.Split(forwardedClientIPHeadersEnv, ","))
- }
- cfg.Server.TrustedProxiesConfigured = trustedProxiesConfigured
- if cfg.Gateway.OpenAIScheduler.StickyEscapeTTFTMs == 0 {
- cfg.Gateway.OpenAIScheduler.StickyEscapeTTFTMs = 15000
- }
- if cfg.Gateway.OpenAIScheduler.StickyEscapeErrorRate == 0 {
- cfg.Gateway.OpenAIScheduler.StickyEscapeErrorRate = 0.5
- }
- // Kept as a backstop: setEnvReachableDefaults now registers this key with its
- // effective default (true), so IsSet always reports true and this branch no
- // longer fires. It still guards the default if that registration is dropped.
- if !cfg.Gateway.OpenAIScheduler.StickyEscapeEnabled && !viper.IsSet("gateway.openai_scheduler.sticky_escape_enabled") {
- cfg.Gateway.OpenAIScheduler.StickyEscapeEnabled = true
- }
- cfg.RunMode = NormalizeRunMode(cfg.RunMode)
- cfg.Server.Mode = strings.ToLower(strings.TrimSpace(cfg.Server.Mode))
- if cfg.Server.Mode == "" {
- cfg.Server.Mode = "debug"
- }
- cfg.Server.FrontendURL = strings.TrimSpace(cfg.Server.FrontendURL)
- cfg.JWT.Secret = strings.TrimSpace(cfg.JWT.Secret)
- cfg.LinuxDo.ClientID = strings.TrimSpace(cfg.LinuxDo.ClientID)
- cfg.LinuxDo.ClientSecret = strings.TrimSpace(cfg.LinuxDo.ClientSecret)
- cfg.LinuxDo.AuthorizeURL = strings.TrimSpace(cfg.LinuxDo.AuthorizeURL)
- cfg.LinuxDo.TokenURL = strings.TrimSpace(cfg.LinuxDo.TokenURL)
- cfg.LinuxDo.UserInfoURL = strings.TrimSpace(cfg.LinuxDo.UserInfoURL)
- cfg.LinuxDo.Scopes = strings.TrimSpace(cfg.LinuxDo.Scopes)
- cfg.LinuxDo.RedirectURL = strings.TrimSpace(cfg.LinuxDo.RedirectURL)
- cfg.LinuxDo.FrontendRedirectURL = strings.TrimSpace(cfg.LinuxDo.FrontendRedirectURL)
- cfg.LinuxDo.TokenAuthMethod = strings.ToLower(strings.TrimSpace(cfg.LinuxDo.TokenAuthMethod))
- cfg.LinuxDo.UserInfoEmailPath = strings.TrimSpace(cfg.LinuxDo.UserInfoEmailPath)
- cfg.LinuxDo.UserInfoIDPath = strings.TrimSpace(cfg.LinuxDo.UserInfoIDPath)
- cfg.LinuxDo.UserInfoUsernamePath = strings.TrimSpace(cfg.LinuxDo.UserInfoUsernamePath)
- applyLegacyWeChatConnectEnvCompatibility(&cfg.WeChat)
- normalizeWeChatConnectConfig(&cfg.WeChat)
- cfg.OIDC.ProviderName = strings.TrimSpace(cfg.OIDC.ProviderName)
- cfg.OIDC.ClientID = strings.TrimSpace(cfg.OIDC.ClientID)
- cfg.OIDC.ClientSecret = strings.TrimSpace(cfg.OIDC.ClientSecret)
- cfg.OIDC.IssuerURL = strings.TrimSpace(cfg.OIDC.IssuerURL)
- cfg.OIDC.DiscoveryURL = strings.TrimSpace(cfg.OIDC.DiscoveryURL)
- cfg.OIDC.AuthorizeURL = strings.TrimSpace(cfg.OIDC.AuthorizeURL)
- cfg.OIDC.TokenURL = strings.TrimSpace(cfg.OIDC.TokenURL)
- cfg.OIDC.UserInfoURL = strings.TrimSpace(cfg.OIDC.UserInfoURL)
- cfg.OIDC.JWKSURL = strings.TrimSpace(cfg.OIDC.JWKSURL)
- cfg.OIDC.Scopes = strings.TrimSpace(cfg.OIDC.Scopes)
- cfg.OIDC.RedirectURL = strings.TrimSpace(cfg.OIDC.RedirectURL)
- cfg.OIDC.FrontendRedirectURL = strings.TrimSpace(cfg.OIDC.FrontendRedirectURL)
- cfg.OIDC.TokenAuthMethod = strings.ToLower(strings.TrimSpace(cfg.OIDC.TokenAuthMethod))
- cfg.OIDC.AllowedSigningAlgs = strings.TrimSpace(cfg.OIDC.AllowedSigningAlgs)
- cfg.OIDC.UserInfoEmailPath = strings.TrimSpace(cfg.OIDC.UserInfoEmailPath)
- cfg.OIDC.UserInfoIDPath = strings.TrimSpace(cfg.OIDC.UserInfoIDPath)
- cfg.OIDC.UserInfoUsernamePath = strings.TrimSpace(cfg.OIDC.UserInfoUsernamePath)
- cfg.OIDC.UsePKCEExplicit = hasExplicitConfigOrEnv("oidc_connect.use_pkce", "OIDC_CONNECT_USE_PKCE")
- cfg.OIDC.ValidateIDTokenExplicit = hasExplicitConfigOrEnv("oidc_connect.validate_id_token", "OIDC_CONNECT_VALIDATE_ID_TOKEN")
- cfg.Dashboard.KeyPrefix = strings.TrimSpace(cfg.Dashboard.KeyPrefix)
- cfg.CORS.AllowedOrigins = normalizeStringSlice(cfg.CORS.AllowedOrigins)
- cfg.Security.ResponseHeaders.AdditionalAllowed = normalizeStringSlice(cfg.Security.ResponseHeaders.AdditionalAllowed)
- cfg.Security.ResponseHeaders.ForceRemove = normalizeStringSlice(cfg.Security.ResponseHeaders.ForceRemove)
- cfg.Security.CSP.Policy = strings.TrimSpace(cfg.Security.CSP.Policy)
- forwardedClientIPHeaders, err := NormalizeForwardedClientIPHeaders(cfg.Security.ForwardedClientIPHeaders)
- if err != nil {
- return nil, fmt.Errorf("security.forwarded_client_ip_headers: %w", err)
- }
- cfg.Security.ForwardedClientIPHeaders = forwardedClientIPHeaders
- cfg.SetForwardedClientIPSettings(cfg.Security.TrustForwardedIPForAPIKeyACL, forwardedClientIPHeaders)
- cfg.Log.Level = strings.ToLower(strings.TrimSpace(cfg.Log.Level))
- cfg.Log.Format = strings.ToLower(strings.TrimSpace(cfg.Log.Format))
- cfg.Log.ServiceName = strings.TrimSpace(cfg.Log.ServiceName)
- cfg.Log.Environment = strings.TrimSpace(cfg.Log.Environment)
- cfg.Log.StacktraceLevel = strings.ToLower(strings.TrimSpace(cfg.Log.StacktraceLevel))
- cfg.Log.Output.FilePath = strings.TrimSpace(cfg.Log.Output.FilePath)
- cfg.Gateway.ForcedCodexInstructionsTemplateFile = strings.TrimSpace(cfg.Gateway.ForcedCodexInstructionsTemplateFile)
- if cfg.Gateway.ForcedCodexInstructionsTemplateFile != "" {
- content, err := os.ReadFile(cfg.Gateway.ForcedCodexInstructionsTemplateFile)
- if err != nil {
- return nil, fmt.Errorf("read forced codex instructions template %q: %w", cfg.Gateway.ForcedCodexInstructionsTemplateFile, err)
- }
- cfg.Gateway.ForcedCodexInstructionsTemplate = string(content)
- }
- // 兼容旧键 gateway.disable_codex_originator_normalization:语义已被
- // disable_codex_identity_enforcement 取代(身份改写升级为强制统一出口),
- // 任一为 true 即关闭强制统一。
- if cfg.Gateway.DisableCodexOriginatorNormalization {
- cfg.Gateway.DisableCodexIdentityEnforcement = true
- }
- // 兼容旧键 gateway.openai_ws.sticky_previous_response_ttl_seconds。
- // 新键未配置(<=0)时回退旧键;新键优先。
- if cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds <= 0 && cfg.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds > 0 {
- cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds = cfg.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds
- }
- // Normalize UMQ mode: 白名单校验,非法值在加载时一次性 warn 并清空
- if m := cfg.Gateway.UserMessageQueue.Mode; m != "" && m != UMQModeSerialize && m != UMQModeThrottle {
- slog.Warn("invalid user_message_queue mode, disabling",
- "mode", m,
- "valid_modes", []string{UMQModeSerialize, UMQModeThrottle})
- cfg.Gateway.UserMessageQueue.Mode = ""
- }
- // Auto-generate TOTP encryption key if not set (32 bytes = 64 hex chars for AES-256)
- cfg.Totp.EncryptionKey = strings.TrimSpace(cfg.Totp.EncryptionKey)
- if cfg.Totp.EncryptionKey == "" {
- key, err := generateJWTSecret(32) // Reuse the same random generation function
- if err != nil {
- return nil, fmt.Errorf("generate totp encryption key error: %w", err)
- }
- cfg.Totp.EncryptionKey = key
- cfg.Totp.EncryptionKeyConfigured = false
- slog.Warn("TOTP encryption key auto-generated. Consider setting a fixed key for production.")
- } else {
- cfg.Totp.EncryptionKeyConfigured = true
- }
- originalJWTSecret := cfg.JWT.Secret
- if allowMissingJWTSecret && originalJWTSecret == "" {
- // 启动阶段允许先无 JWT 密钥,后续在数据库初始化后补齐。
- cfg.JWT.Secret = strings.Repeat("0", 32)
- }
- if err := cfg.Validate(); err != nil {
- return nil, fmt.Errorf("validate config error: %w", err)
- }
- if allowMissingJWTSecret && originalJWTSecret == "" {
- cfg.JWT.Secret = ""
- }
- if !cfg.Security.URLAllowlist.Enabled {
- slog.Warn("security.url_allowlist.enabled=false; allowlist/SSRF checks disabled (minimal format validation only).")
- }
- if !cfg.Security.ResponseHeaders.Enabled {
- slog.Warn("security.response_headers.enabled=false; configurable header filtering disabled (default allowlist only).")
- }
- if cfg.JWT.Secret != "" && isWeakJWTSecret(cfg.JWT.Secret) {
- slog.Warn("JWT secret appears weak; use a 32+ character random secret in production.")
- }
- if len(cfg.Security.ResponseHeaders.AdditionalAllowed) > 0 || len(cfg.Security.ResponseHeaders.ForceRemove) > 0 {
- slog.Info("response header policy configured",
- "additional_allowed", cfg.Security.ResponseHeaders.AdditionalAllowed,
- "force_remove", cfg.Security.ResponseHeaders.ForceRemove,
- )
- }
- return &cfg, nil
- }
- func configureConfigSource(setConfigFile, addConfigPath func(string)) {
- if configFile := strings.TrimSpace(os.Getenv("CONFIG_FILE")); configFile != "" {
- setConfigFile(configFile)
- return
- }
- // Add config paths in priority order.
- if dataDir := strings.TrimSpace(os.Getenv("DATA_DIR")); dataDir != "" {
- addConfigPath(dataDir)
- }
- addConfigPath("/app/data")
- addConfigPath(".")
- addConfigPath("./config")
- addConfigPath("/etc/sub2api")
- }
- func setDefaults() {
- viper.SetDefault("run_mode", RunModeStandard)
- // Server
- viper.SetDefault("server.host", "0.0.0.0")
- viper.SetDefault("server.port", 8080)
- viper.SetDefault("server.mode", "release")
- viper.SetDefault("server.enable_server_timing", false)
- viper.SetDefault("server.frontend_url", "")
- viper.SetDefault("server.read_header_timeout", 10) // 10秒读取请求头
- viper.SetDefault("server.max_header_bytes", 64*1024)
- viper.SetDefault("server.idle_timeout", 120) // 120秒空闲超时
- viper.SetDefault("server.max_request_body_size", int64(256*1024*1024))
- // H2C 默认配置
- viper.SetDefault("server.h2c.enabled", false)
- viper.SetDefault("server.h2c.max_concurrent_streams", uint32(50)) // 50 个并发流
- viper.SetDefault("server.h2c.idle_timeout", 75) // 75 秒
- viper.SetDefault("server.h2c.max_read_frame_size", 1<<20) // 1MB(够用)
- viper.SetDefault("server.h2c.max_upload_buffer_per_connection", 2<<20) // 2MB
- viper.SetDefault("server.h2c.max_upload_buffer_per_stream", 512<<10) // 512KB
- // Log
- viper.SetDefault("log.level", "info")
- viper.SetDefault("log.format", "console")
- viper.SetDefault("log.service_name", "sub2api")
- viper.SetDefault("log.env", "production")
- viper.SetDefault("log.caller", true)
- viper.SetDefault("log.stacktrace_level", "error")
- viper.SetDefault("log.output.to_stdout", true)
- viper.SetDefault("log.output.to_file", true)
- viper.SetDefault("log.output.file_path", "")
- viper.SetDefault("log.rotation.max_size_mb", 100)
- viper.SetDefault("log.rotation.max_backups", 10)
- viper.SetDefault("log.rotation.max_age_days", 7)
- viper.SetDefault("log.rotation.compress", true)
- viper.SetDefault("log.rotation.local_time", true)
- viper.SetDefault("log.sampling.enabled", false)
- viper.SetDefault("log.sampling.initial", 100)
- viper.SetDefault("log.sampling.thereafter", 100)
- // CORS
- viper.SetDefault("cors.allowed_origins", []string{})
- viper.SetDefault("cors.allow_credentials", true)
- // WebAuthn / Passkeys are opt-in because every deployment must explicitly
- // declare its relying-party domain and trusted browser origins.
- viper.SetDefault("webauthn.enabled", false)
- viper.SetDefault("webauthn.rp_display_name", "Sub2API")
- viper.SetDefault("webauthn.rp_id", "")
- viper.SetDefault("webauthn.rp_origins", []string{})
- // Security
- viper.SetDefault("security.url_allowlist.enabled", false)
- viper.SetDefault("security.url_allowlist.upstream_hosts", []string{
- "api.openai.com",
- "api.anthropic.com",
- "api.kimi.com",
- "api.moonshot.ai",
- "api.moonshot.cn",
- "open.bigmodel.cn",
- "api.minimaxi.com", // MiniMax CN quota + inference
- "api.minimax.io", // MiniMax intl; frozen allowlists must add this host to use the intl site
- "generativelanguage.googleapis.com",
- "cloudcode-pa.googleapis.com",
- "*.openai.azure.com",
- })
- viper.SetDefault("security.url_allowlist.pricing_hosts", []string{
- "raw.githubusercontent.com",
- })
- viper.SetDefault("security.url_allowlist.crs_hosts", []string{})
- viper.SetDefault("security.url_allowlist.allow_private_hosts", true)
- viper.SetDefault("security.url_allowlist.allow_insecure_http", true)
- viper.SetDefault("security.response_headers.enabled", true)
- viper.SetDefault("security.response_headers.additional_allowed", []string{})
- viper.SetDefault("security.response_headers.force_remove", []string{})
- viper.SetDefault("security.csp.enabled", true)
- viper.SetDefault("security.csp.policy", DefaultCSPPolicy)
- viper.SetDefault("security.proxy_probe.insecure_skip_verify", false)
- viper.SetDefault("security.trust_forwarded_ip_for_api_key_acl", true)
- // Security - disable direct fallback on proxy error
- viper.SetDefault("security.proxy_fallback.allow_direct_on_error", false)
- // Billing
- viper.SetDefault("billing.circuit_breaker.enabled", true)
- viper.SetDefault("billing.circuit_breaker.failure_threshold", 5)
- viper.SetDefault("billing.circuit_breaker.reset_timeout_seconds", 30)
- viper.SetDefault("billing.circuit_breaker.half_open_requests", 3)
- viper.SetDefault("billing.minimum_balance_reserve", 0.000001)
- viper.SetDefault("billing.user_platform_quota_cache_ttl_seconds", 86400)
- viper.SetDefault("billing.user_platform_quota_sentinel_ttl_seconds", 3600)
- // Turnstile
- viper.SetDefault("turnstile.required", false)
- // LinuxDo Connect OAuth 登录
- viper.SetDefault("linuxdo_connect.enabled", false)
- viper.SetDefault("linuxdo_connect.client_id", "")
- viper.SetDefault("linuxdo_connect.client_secret", "")
- viper.SetDefault("linuxdo_connect.authorize_url", "https://connect.linux.do/oauth2/authorize")
- viper.SetDefault("linuxdo_connect.token_url", "https://connect.linux.do/oauth2/token")
- viper.SetDefault("linuxdo_connect.userinfo_url", "https://connect.linux.do/api/user")
- viper.SetDefault("linuxdo_connect.scopes", "user")
- viper.SetDefault("linuxdo_connect.redirect_url", "")
- viper.SetDefault("linuxdo_connect.frontend_redirect_url", "/auth/linuxdo/callback")
- viper.SetDefault("linuxdo_connect.token_auth_method", "client_secret_post")
- viper.SetDefault("linuxdo_connect.use_pkce", false)
- viper.SetDefault("linuxdo_connect.userinfo_email_path", "")
- viper.SetDefault("linuxdo_connect.userinfo_id_path", "")
- viper.SetDefault("linuxdo_connect.userinfo_username_path", "")
- // WeChat Connect OAuth 登录
- viper.SetDefault("wechat_connect.enabled", false)
- viper.SetDefault("wechat_connect.app_id", "")
- viper.SetDefault("wechat_connect.app_secret", "")
- viper.SetDefault("wechat_connect.open_app_id", "")
- viper.SetDefault("wechat_connect.open_app_secret", "")
- viper.SetDefault("wechat_connect.mp_app_id", "")
- viper.SetDefault("wechat_connect.mp_app_secret", "")
- viper.SetDefault("wechat_connect.mobile_app_id", "")
- viper.SetDefault("wechat_connect.mobile_app_secret", "")
- viper.SetDefault("wechat_connect.open_enabled", false)
- viper.SetDefault("wechat_connect.mp_enabled", false)
- viper.SetDefault("wechat_connect.mobile_enabled", false)
- viper.SetDefault("wechat_connect.mode", defaultWeChatConnectMode)
- viper.SetDefault("wechat_connect.scopes", defaultWeChatConnectScopes)
- viper.SetDefault("wechat_connect.redirect_url", "")
- viper.SetDefault("wechat_connect.frontend_redirect_url", defaultWeChatConnectFrontendRedirect)
- // Generic OIDC OAuth 登录
- viper.SetDefault("oidc_connect.enabled", false)
- viper.SetDefault("oidc_connect.provider_name", "OIDC")
- viper.SetDefault("oidc_connect.client_id", "")
- viper.SetDefault("oidc_connect.client_secret", "")
- viper.SetDefault("oidc_connect.issuer_url", "")
- viper.SetDefault("oidc_connect.discovery_url", "")
- viper.SetDefault("oidc_connect.authorize_url", "")
- viper.SetDefault("oidc_connect.token_url", "")
- viper.SetDefault("oidc_connect.userinfo_url", "")
- viper.SetDefault("oidc_connect.jwks_url", "")
- viper.SetDefault("oidc_connect.scopes", "openid email profile")
- viper.SetDefault("oidc_connect.redirect_url", "")
- viper.SetDefault("oidc_connect.frontend_redirect_url", "/auth/oidc/callback")
- viper.SetDefault("oidc_connect.token_auth_method", "client_secret_post")
- viper.SetDefault("oidc_connect.use_pkce", true)
- viper.SetDefault("oidc_connect.validate_id_token", true)
- viper.SetDefault("oidc_connect.allowed_signing_algs", "RS256,ES256,PS256")
- viper.SetDefault("oidc_connect.clock_skew_seconds", 120)
- viper.SetDefault("oidc_connect.require_email_verified", false)
- viper.SetDefault("oidc_connect.userinfo_email_path", "")
- viper.SetDefault("oidc_connect.userinfo_id_path", "")
- viper.SetDefault("oidc_connect.userinfo_username_path", "")
- // DingTalk Connect OAuth 登录
- viper.SetDefault("dingtalk_connect.enabled", false)
- viper.SetDefault("dingtalk_connect.authorize_url", "https://login.dingtalk.com/oauth2/auth")
- viper.SetDefault("dingtalk_connect.token_url", "https://api.dingtalk.com/v1.0/oauth2/userAccessToken")
- viper.SetDefault("dingtalk_connect.userinfo_url", "https://api.dingtalk.com/v1.0/contact/users/me")
- viper.SetDefault("dingtalk_connect.scopes", "openid")
- viper.SetDefault("dingtalk_connect.frontend_redirect_url", "/auth/dingtalk/callback")
- viper.SetDefault("dingtalk_connect.dingtalk_app_kind", "internal_app")
- viper.SetDefault("dingtalk_connect.app_type", "public")
- viper.SetDefault("dingtalk_connect.corp_restriction_policy", "none")
- viper.SetDefault("dingtalk_connect.require_email", true)
- viper.SetDefault("dingtalk_connect.username_overwrite_policy", "if_empty")
- // Database
- viper.SetDefault("database.host", "localhost")
- viper.SetDefault("database.port", 5432)
- viper.SetDefault("database.user", "postgres")
- viper.SetDefault("database.password", "postgres")
- viper.SetDefault("database.dbname", "sub2api")
- viper.SetDefault("database.sslmode", "prefer")
- viper.SetDefault("database.max_open_conns", 256)
- viper.SetDefault("database.max_idle_conns", 128)
- viper.SetDefault("database.conn_max_lifetime_minutes", 30)
- viper.SetDefault("database.conn_max_idle_time_minutes", 5)
- viper.SetDefault("database.user_platform_quota_flusher_enabled", false)
- viper.SetDefault("database.user_platform_quota_flush_interval_ms", 2000)
- viper.SetDefault("database.user_platform_quota_flush_batch_size", 1000)
- // Redis
- viper.SetDefault("redis.host", "localhost")
- viper.SetDefault("redis.port", 6379)
- viper.SetDefault("redis.username", "")
- viper.SetDefault("redis.password", "")
- viper.SetDefault("redis.db", 0)
- viper.SetDefault("redis.dial_timeout_seconds", 5)
- viper.SetDefault("redis.read_timeout_seconds", 3)
- viper.SetDefault("redis.write_timeout_seconds", 3)
- viper.SetDefault("redis.pool_size", 1024)
- viper.SetDefault("redis.min_idle_conns", 128)
- viper.SetDefault("redis.enable_tls", false)
- // Batch Image queue
- viper.SetDefault("batch_image.enabled", false)
- viper.SetDefault("batch_image.max_items_per_job_default", 200)
- viper.SetDefault("batch_image.max_items_per_job_trial", 50)
- viper.SetDefault("batch_image.max_output_images_per_job", 200)
- viper.SetDefault("batch_image.max_output_images_per_item", 4)
- viper.SetDefault("batch_image.max_prompt_chars_per_item", 8000)
- viper.SetDefault("batch_image.max_reference_images_per_job", 1000)
- viper.SetDefault("batch_image.max_reference_inline_bytes_per_job", 134217728)
- viper.SetDefault("batch_image.default_response_mime_type", "image/png")
- viper.SetDefault("batch_image.default_image_size", "1K")
- viper.SetDefault("batch_image.max_download_items_zip", 200)
- viper.SetDefault("batch_image.max_download_bytes_per_request", 536870912)
- viper.SetDefault("batch_image.max_download_duration_seconds", 600)
- viper.SetDefault("batch_image.max_download_concurrency_per_user", 1)
- viper.SetDefault("batch_image.input_retention_after_terminal_hours", 24)
- viper.SetDefault("batch_image.output_retention_after_terminal_hours", 72)
- viper.SetDefault("batch_image.output_retention_max_days", 7)
- viper.SetDefault("batch_image.cleanup_interval_minutes", 30)
- viper.SetDefault("batch_image.cleanup_batch_size", 100)
- viper.SetDefault("batch_image.queue_enabled", false)
- viper.SetDefault("batch_image.queue_ready_key", "batch_image:queue:ready")
- viper.SetDefault("batch_image.queue_delayed_key", "batch_image:queue:delayed")
- viper.SetDefault("batch_image.queue_active_key", "batch_image:queue:active")
- viper.SetDefault("batch_image.inflight_key_prefix", "batch_image:queue:inflight:")
- viper.SetDefault("batch_image.lock_key_prefix", "batch_image:queue:lock:")
- viper.SetDefault("batch_image.idempotency_key_prefix", "batch_image:queue:idem:")
- viper.SetDefault("batch_image.inflight_ttl_seconds", 604800)
- viper.SetDefault("batch_image.job_lock_ttl_seconds", 300)
- viper.SetDefault("batch_image.default_requeue_delay_seconds", 30)
- viper.SetDefault("batch_image.error_retry_delay_seconds", 60)
- viper.SetDefault("batch_image.lock_conflict_delay_seconds", 5)
- viper.SetDefault("batch_image.stale_active_after_seconds", 600)
- viper.SetDefault("batch_image.delayed_mover_interval_seconds", 5)
- viper.SetDefault("batch_image.recovery_interval_seconds", 300)
- viper.SetDefault("batch_image.delayed_move_limit", 100)
- viper.SetDefault("batch_image.recover_limit", 100)
- viper.SetDefault("batch_image.vertex_enabled", false)
- viper.SetDefault("batch_image.vertex_project_id", "")
- viper.SetDefault("batch_image.vertex_location", "global")
- viper.SetDefault("batch_image.vertex_managed_gcs_bucket", "")
- viper.SetDefault("batch_image.vertex_managed_gcs_prefix", "batch-image/{env}/{batch_id}")
- viper.SetDefault("batch_image.vertex_input_retention_hours", 24)
- viper.SetDefault("batch_image.vertex_output_retention_hours", 72)
- viper.SetDefault("batch_image.vertex_batch_prediction_base_url", "")
- viper.SetDefault("batch_image.vertex_gcs_base_url", "")
- // Image storage (async image task result offload to S3-compatible object storage)
- viper.SetDefault("image_storage.enabled", false)
- viper.SetDefault("image_storage.region", "auto")
- viper.SetDefault("image_storage.prefix", "images/")
- viper.SetDefault("image_storage.force_path_style", false)
- viper.SetDefault("image_storage.presign_expiry_hours", 24)
- viper.SetDefault("image_storage.max_download_bytes", 33554432)
- // Registered with empty defaults so AutomaticEnv can reach them: viper only
- // decodes keys present in AllKeys(), so a credential that is supplied purely
- // via IMAGE_STORAGE_* and never appears in config.yaml would be dropped and
- // silently disable the whole async image feature.
- viper.SetDefault("image_storage.endpoint", "")
- viper.SetDefault("image_storage.bucket", "")
- viper.SetDefault("image_storage.access_key_id", "")
- viper.SetDefault("image_storage.secret_access_key", "")
- viper.SetDefault("image_storage.public_base_url", "")
- // Ops (vNext)
- viper.SetDefault("ops.enabled", true)
- viper.SetDefault("ops.use_preaggregated_tables", true)
- viper.SetDefault("ops.cleanup.enabled", true)
- viper.SetDefault("ops.cleanup.schedule", "0 2 * * *")
- // Retention days: vNext defaults to 30 days across ops datasets.
- viper.SetDefault("ops.cleanup.error_log_retention_days", 30)
- viper.SetDefault("ops.cleanup.system_log_retention_days", 30)
- viper.SetDefault("ops.cleanup.minute_metrics_retention_days", 30)
- viper.SetDefault("ops.cleanup.hourly_metrics_retention_days", 30)
- viper.SetDefault("ops.aggregation.enabled", true)
- viper.SetDefault("ops.metrics_collector_cache.enabled", true)
- // TTL should be slightly larger than collection interval (1m) to maximize cross-replica cache hits.
- viper.SetDefault("ops.metrics_collector_cache.ttl", 65*time.Second)
- // JWT
- viper.SetDefault("jwt.secret", "")
- viper.SetDefault("jwt.expire_hour", 24)
- viper.SetDefault("jwt.access_token_expire_minutes", 0) // 0 表示回退到 expire_hour
- viper.SetDefault("jwt.refresh_token_expire_days", 30) // 30天Refresh Token有效期
- viper.SetDefault("jwt.refresh_window_minutes", 2) // 过期前2分钟开始允许刷新
- // TOTP
- viper.SetDefault("totp.encryption_key", "")
- // Default
- // Admin credentials are created via the setup flow (web wizard / CLI / AUTO_SETUP).
- // Do not ship fixed defaults here to avoid insecure "known credentials" in production.
- viper.SetDefault("default.admin_email", "")
- viper.SetDefault("default.admin_password", "")
- viper.SetDefault("default.user_concurrency", 5)
- viper.SetDefault("default.user_balance", 0)
- viper.SetDefault("default.api_key_prefix", "sk-")
- viper.SetDefault("default.rate_multiplier", 1.0)
- // RateLimit
- viper.SetDefault("rate_limit.overload_cooldown_minutes", 10)
- viper.SetDefault("rate_limit.oauth_401_cooldown_minutes", 10)
- // Pricing - 从 model-price-repo main 分支同步模型定价和上下文窗口数据
- viper.SetDefault("pricing.remote_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/main/model_prices_and_context_window.json")
- viper.SetDefault("pricing.hash_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/main/model_prices_and_context_window.sha256")
- viper.SetDefault("pricing.data_dir", "./data")
- viper.SetDefault("pricing.fallback_file", "./resources/model-pricing/model_prices_and_context_window.json")
- viper.SetDefault("pricing.override_file", "")
- viper.SetDefault("pricing.update_interval_hours", 24)
- viper.SetDefault("pricing.hash_check_interval_minutes", 10)
- // 本地进程插件。插件必须由管理员手动上传,项目默认不携带任何插件能力。
- viper.SetDefault("plugins.data_dir", "")
- viper.SetDefault("plugins.allow_unsigned", false)
- viper.SetDefault("plugins.trusted_publishers", map[string]string{})
- viper.SetDefault("plugins.max_upload_bytes", int64(128*1024*1024))
- viper.SetDefault("plugins.max_uncompressed_bytes", int64(256*1024*1024))
- viper.SetDefault("plugins.start_timeout_seconds", 15)
- // Timezone (default to Asia/Shanghai for Chinese users)
- viper.SetDefault("timezone", "Asia/Shanghai")
- // API Key auth cache
- viper.SetDefault("api_key_auth_cache.l1_size", 65535)
- viper.SetDefault("api_key_auth_cache.l1_ttl_seconds", 15)
- viper.SetDefault("api_key_auth_cache.l2_ttl_seconds", 300)
- viper.SetDefault("api_key_auth_cache.negative_ttl_seconds", 30)
- viper.SetDefault("api_key_auth_cache.jitter_percent", 10)
- viper.SetDefault("api_key_auth_cache.singleflight", true)
- viper.SetDefault("api_key_auth_cache.lookup_concurrency", 64)
- viper.SetDefault("api_key_auth_cache.invalid_abuse.enabled", true)
- viper.SetDefault("api_key_auth_cache.invalid_abuse.threshold", 120)
- viper.SetDefault("api_key_auth_cache.invalid_abuse.window_seconds", 60)
- viper.SetDefault("api_key_auth_cache.invalid_abuse.block_seconds", 60)
- viper.SetDefault("api_key_auth_cache.invalid_abuse.capacity", 16384)
- // Subscription auth L1 cache
- viper.SetDefault("subscription_cache.l1_size", 16384)
- viper.SetDefault("subscription_cache.l1_ttl_seconds", 10)
- viper.SetDefault("subscription_cache.jitter_percent", 10)
- // Dashboard cache
- viper.SetDefault("dashboard_cache.enabled", true)
- viper.SetDefault("dashboard_cache.key_prefix", "sub2api:")
- viper.SetDefault("dashboard_cache.stats_fresh_ttl_seconds", 15)
- viper.SetDefault("dashboard_cache.stats_ttl_seconds", 30)
- viper.SetDefault("dashboard_cache.stats_refresh_timeout_seconds", 30)
- // Dashboard aggregation
- viper.SetDefault("dashboard_aggregation.enabled", true)
- viper.SetDefault("dashboard_aggregation.interval_seconds", 60)
- viper.SetDefault("dashboard_aggregation.lookback_seconds", 120)
- viper.SetDefault("dashboard_aggregation.backfill_enabled", false)
- viper.SetDefault("dashboard_aggregation.backfill_max_days", 31)
- viper.SetDefault("dashboard_aggregation.retention.usage_logs_days", 90)
- viper.SetDefault("dashboard_aggregation.retention.usage_billing_dedup_days", 365)
- viper.SetDefault("dashboard_aggregation.retention.hourly_days", 180)
- viper.SetDefault("dashboard_aggregation.retention.daily_days", 730)
- viper.SetDefault("dashboard_aggregation.recompute_days", 2)
- // Usage cleanup task
- viper.SetDefault("usage_cleanup.enabled", true)
- viper.SetDefault("usage_cleanup.max_range_days", 31)
- viper.SetDefault("usage_cleanup.batch_size", 5000)
- viper.SetDefault("usage_cleanup.worker_interval_seconds", 10)
- viper.SetDefault("usage_cleanup.task_timeout_seconds", 1800)
- // Idempotency
- viper.SetDefault("idempotency.observe_only", true)
- viper.SetDefault("idempotency.default_ttl_seconds", 86400)
- viper.SetDefault("idempotency.system_operation_ttl_seconds", 3600)
- viper.SetDefault("idempotency.processing_timeout_seconds", 30)
- viper.SetDefault("idempotency.failed_retry_backoff_seconds", 5)
- viper.SetDefault("idempotency.max_stored_response_len", 64*1024)
- viper.SetDefault("idempotency.cleanup_interval_seconds", 60)
- viper.SetDefault("idempotency.cleanup_batch_size", 500)
- // Gateway
- viper.SetDefault("gateway.response_header_timeout", 600) // 600秒(10分钟)等待上游响应头,LLM高负载时可能排队较久
- viper.SetDefault("gateway.openai_response_header_timeout", 0)
- viper.SetDefault("gateway.grok_response_header_timeout", 120)
- viper.SetDefault("gateway.openai_first_output_timeout_seconds", 0)
- viper.SetDefault("gateway.openai_high_effort_first_output_timeout_seconds", 0)
- viper.SetDefault("gateway.log_upstream_error_body", true)
- viper.SetDefault("gateway.log_upstream_error_body_max_bytes", 2048)
- viper.SetDefault("gateway.inject_beta_for_apikey", false)
- viper.SetDefault("gateway.failover_on_400", false)
- viper.SetDefault("gateway.max_account_switches", 10)
- viper.SetDefault("gateway.max_account_switches_gemini", 3)
- viper.SetDefault("gateway.force_codex_cli", false)
- viper.SetDefault("gateway.disable_codex_identity_enforcement", false)
- viper.SetDefault("gateway.disable_codex_originator_normalization", false)
- viper.SetDefault("gateway.codex_image_generation_bridge_enabled", false)
- viper.SetDefault("gateway.openai_passthrough_allow_timeout_headers", false)
- viper.SetDefault("gateway.openai_compact_model", "gpt-5.4")
- viper.SetDefault("gateway.live.max_session_duration_seconds", 3600)
- // OpenAI Responses WebSocket(默认开启;可通过 force_http 紧急回滚)
- viper.SetDefault("gateway.openai_ws.enabled", true)
- viper.SetDefault("gateway.openai_ws.mode_router_v2_enabled", false)
- viper.SetDefault("gateway.openai_ws.ingress_mode_default", "ctx_pool")
- viper.SetDefault("gateway.openai_ws.client_first_message_timeout_seconds", DefaultOpenAIWSClientFirstMessageTimeoutSeconds)
- viper.SetDefault("gateway.openai_ws.ingress_inter_turn_idle_timeout_seconds", 300)
- viper.SetDefault("gateway.openai_ws.max_ingress_connections_per_api_key", 64)
- viper.SetDefault("gateway.openai_ws.oauth_enabled", true)
- viper.SetDefault("gateway.openai_ws.apikey_enabled", true)
- viper.SetDefault("gateway.openai_ws.force_http", false)
- viper.SetDefault("gateway.openai_ws.allow_store_recovery", false)
- viper.SetDefault("gateway.openai_ws.ingress_previous_response_recovery_enabled", true)
- viper.SetDefault("gateway.openai_ws.store_disabled_conn_mode", "strict")
- viper.SetDefault("gateway.openai_ws.store_disabled_force_new_conn", true)
- viper.SetDefault("gateway.openai_ws.prewarm_generate_enabled", false)
- viper.SetDefault("gateway.openai_ws.client_read_limit_bytes", 64*1024*1024)
- viper.SetDefault("gateway.openai_ws.http_bridge_enabled", true)
- viper.SetDefault("gateway.openai_ws.http_bridge_threshold_bytes", 15*1024*1024)
- viper.SetDefault("gateway.openai_ws.responses_websockets", false)
- viper.SetDefault("gateway.openai_ws.responses_websockets_v2", true)
- viper.SetDefault("gateway.openai_ws.max_conns_per_account", 128)
- viper.SetDefault("gateway.openai_ws.min_idle_per_account", 4)
- viper.SetDefault("gateway.openai_ws.max_idle_per_account", 12)
- viper.SetDefault("gateway.openai_ws.dynamic_max_conns_by_account_concurrency_enabled", true)
- viper.SetDefault("gateway.openai_ws.oauth_max_conns_factor", 1.0)
- viper.SetDefault("gateway.openai_ws.apikey_max_conns_factor", 1.0)
- viper.SetDefault("gateway.openai_ws.dial_timeout_seconds", 10)
- viper.SetDefault("gateway.openai_ws.read_timeout_seconds", 900)
- viper.SetDefault("gateway.openai_ws.write_timeout_seconds", 120)
- viper.SetDefault("gateway.openai_ws.pool_target_utilization", 0.7)
- viper.SetDefault("gateway.openai_ws.queue_limit_per_conn", 64)
- viper.SetDefault("gateway.openai_ws.event_flush_batch_size", 1)
- viper.SetDefault("gateway.openai_ws.event_flush_interval_ms", 10)
- viper.SetDefault("gateway.openai_ws.prewarm_cooldown_ms", 300)
- viper.SetDefault("gateway.openai_ws.fallback_cooldown_seconds", 30)
- viper.SetDefault("gateway.openai_ws.retry_backoff_initial_ms", 120)
- viper.SetDefault("gateway.openai_ws.retry_backoff_max_ms", 2000)
- viper.SetDefault("gateway.openai_ws.retry_jitter_ratio", 0.2)
- viper.SetDefault("gateway.openai_ws.retry_total_budget_ms", 5000)
- viper.SetDefault("gateway.openai_ws.payload_log_sample_rate", 0.2)
- viper.SetDefault("gateway.openai_ws.lb_top_k", 7)
- viper.SetDefault("gateway.openai_ws.sticky_session_ttl_seconds", 3600)
- viper.SetDefault("gateway.openai_ws.session_hash_read_old_fallback", true)
- viper.SetDefault("gateway.openai_ws.session_hash_dual_write_old", true)
- viper.SetDefault("gateway.openai_ws.metadata_bridge_enabled", true)
- viper.SetDefault("gateway.openai_ws.sticky_response_id_ttl_seconds", 3600)
- viper.SetDefault("gateway.openai_ws.sticky_previous_response_ttl_seconds", 3600)
- viper.SetDefault("gateway.openai_ws.scheduler_score_weights.priority", 1.0)
- viper.SetDefault("gateway.openai_ws.scheduler_score_weights.load", 1.0)
- viper.SetDefault("gateway.openai_ws.scheduler_score_weights.queue", 0.7)
- viper.SetDefault("gateway.openai_ws.scheduler_score_weights.error_rate", 0.8)
- viper.SetDefault("gateway.openai_ws.scheduler_score_weights.ttft", 0.5)
- viper.SetDefault("gateway.openai_ws.scheduler_score_weights.reset", 0.0)
- viper.SetDefault("gateway.openai_ws.scheduler_score_weights.quota_headroom", 0.0)
- viper.SetDefault("gateway.openai_ws.scheduler_score_weights.upstream_cost", 0.0)
- viper.SetDefault("gateway.openai_ws.scheduler_score_weights.previous_response", 5.0)
- viper.SetDefault("gateway.openai_ws.scheduler_score_weights.session_sticky", 3.0)
- // OpenAI HTTP upstream protocol strategy
- viper.SetDefault("gateway.openai_http2.enabled", true)
- viper.SetDefault("gateway.openai_http2.allow_proxy_fallback_to_http1", true)
- viper.SetDefault("gateway.openai_http2.fallback_error_threshold", 2)
- viper.SetDefault("gateway.openai_http2.fallback_window_seconds", 60)
- viper.SetDefault("gateway.openai_http2.fallback_ttl_seconds", 600)
- viper.SetDefault("gateway.openai_proxy_stream_circuit.disabled", false)
- viper.SetDefault("gateway.openai_proxy_stream_circuit.failure_threshold", 2)
- viper.SetDefault("gateway.openai_proxy_stream_circuit.window_seconds", 60)
- viper.SetDefault("gateway.openai_proxy_stream_circuit.ttl_seconds", 600)
- // Grok free-tier local soft gate (scheduler-only; admin QueryQuota does not use this).
- // Enabled by default because free detection requires an explicit free tier marker.
- viper.SetDefault("gateway.grok.free_quota_soft_gate_enabled", true)
- viper.SetDefault("gateway.grok.password_auth_enabled", false)
- // Free soft-gate nominal limit: 500k tokens / rolling 24h (operator policy).
- viper.SetDefault("gateway.grok.free_quota_token_limit", int64(500_000))
- viper.SetDefault("gateway.grok.free_quota_soft_gate_percent", 95)
- viper.SetDefault("gateway.grok.free_quota_window_hours", 24)
- viper.SetDefault("gateway.grok.free_quota_stats_cache_seconds", 60)
- // 国产供应商余额检测(kimi/deepseek payg;zhipu 无余额端点,仅靠响应式 429/402)。
- viper.SetDefault("gateway.cn_providers.balance_check_enabled", true)
- viper.SetDefault("gateway.cn_providers.balance_threshold", 0.5)
- viper.SetDefault("gateway.cn_providers.balance_check_interval_minutes", 10)
- viper.SetDefault("gateway.image_concurrency.enabled", false)
- viper.SetDefault("gateway.image_concurrency.max_concurrent_requests", 0)
- viper.SetDefault("gateway.image_concurrency.overflow_mode", ImageConcurrencyOverflowModeReject)
- viper.SetDefault("gateway.image_concurrency.wait_timeout_seconds", 30)
- viper.SetDefault("gateway.image_concurrency.max_waiting_requests", 100)
- viper.SetDefault("gateway.antigravity_fallback_cooldown_minutes", 1)
- viper.SetDefault("gateway.antigravity_extra_retries", 10)
- viper.SetDefault("gateway.max_body_size", int64(256*1024*1024))
- viper.SetDefault("gateway.text_max_body_size", int64(32*1024*1024))
- viper.SetDefault("gateway.upstream_response_read_max_bytes", DefaultUpstreamResponseReadMaxBytes)
- viper.SetDefault("gateway.models_list_read_max_bytes", DefaultModelsListReadMaxBytes)
- viper.SetDefault("gateway.proxy_probe_response_read_max_bytes", int64(1024*1024))
- viper.SetDefault("gateway.gemini_debug_response_headers", false)
- viper.SetDefault("gateway.connection_pool_isolation", ConnectionPoolIsolationAccountProxy)
- // HTTP 上游连接池配置(针对 5000+ 并发用户优化)
- viper.SetDefault("gateway.max_idle_conns", 2560) // 最大空闲连接总数(高并发场景可调大)
- viper.SetDefault("gateway.max_idle_conns_per_host", 120) // 每主机最大空闲连接(HTTP/2 场景默认)
- viper.SetDefault("gateway.max_conns_per_host", 1024) // 每主机最大连接数(含活跃;流式/HTTP1.1 场景可调大,如 2400+)
- viper.SetDefault("gateway.idle_conn_timeout_seconds", 90) // 空闲连接超时(秒)
- viper.SetDefault("gateway.max_upstream_clients", 5000)
- viper.SetDefault("gateway.client_idle_ttl_seconds", 900)
- viper.SetDefault("gateway.concurrency_slot_ttl_minutes", 30) // 并发槽位过期时间(支持超长请求)
- viper.SetDefault("gateway.stream_data_interval_timeout", 180)
- viper.SetDefault("gateway.stream_keepalive_interval", 10)
- viper.SetDefault("gateway.image_stream_data_interval_timeout", 900)
- viper.SetDefault("gateway.image_stream_keepalive_interval", 10)
- viper.SetDefault("gateway.image_nonstream_keepalive_interval", 0)
- viper.SetDefault("gateway.max_line_size", 500*1024*1024)
- viper.SetDefault("gateway.scheduling.sticky_session_max_waiting", 3)
- viper.SetDefault("gateway.scheduling.sticky_session_wait_timeout", 120*time.Second)
- viper.SetDefault("gateway.scheduling.fallback_wait_timeout", 30*time.Second)
- viper.SetDefault("gateway.scheduling.fallback_max_waiting", 100)
- viper.SetDefault("gateway.scheduling.fallback_selection_mode", "last_used")
- viper.SetDefault("gateway.scheduling.prefer_soonest_reset", false)
- viper.SetDefault("gateway.scheduling.load_batch_enabled", true)
- viper.SetDefault("gateway.scheduling.load_batch_cache_ttl_ms", 200)
- viper.SetDefault("gateway.scheduling.snapshot_mget_chunk_size", 128)
- viper.SetDefault("gateway.scheduling.snapshot_write_chunk_size", 256)
- viper.SetDefault("gateway.scheduling.slot_cleanup_interval", 30*time.Second)
- viper.SetDefault("gateway.scheduling.db_fallback_enabled", true)
- viper.SetDefault("gateway.scheduling.db_fallback_timeout_seconds", 0)
- viper.SetDefault("gateway.scheduling.db_fallback_max_qps", 0)
- viper.SetDefault("gateway.scheduling.outbox_poll_interval_seconds", 1)
- viper.SetDefault("gateway.scheduling.outbox_lag_warn_seconds", 5)
- viper.SetDefault("gateway.scheduling.outbox_lag_rebuild_seconds", 10)
- viper.SetDefault("gateway.scheduling.outbox_lag_rebuild_failures", 3)
- viper.SetDefault("gateway.scheduling.outbox_backlog_rebuild_rows", 10000)
- viper.SetDefault("gateway.scheduling.full_rebuild_interval_seconds", 300)
- viper.SetDefault("gateway.usage_record.worker_count", 128)
- viper.SetDefault("gateway.usage_record.queue_size", 16384)
- viper.SetDefault("gateway.usage_record.task_timeout_seconds", 5)
- // 默认 sync:队列满时由提交方内联执行(提交点在响应写出之后,不阻塞客户端)。
- // sample/drop 会在溢出时静默丢弃计费任务,造成扣费与 usage_logs 对账缺口(issue #3656),
- // 仅供显式配置的运维场景使用。
- viper.SetDefault("gateway.usage_record.overflow_policy", UsageRecordOverflowPolicySync)
- viper.SetDefault("gateway.usage_record.overflow_sample_percent", 10)
- viper.SetDefault("gateway.usage_record.auto_scale_enabled", true)
- viper.SetDefault("gateway.usage_record.auto_scale_min_workers", 128)
- viper.SetDefault("gateway.usage_record.auto_scale_max_workers", 512)
- viper.SetDefault("gateway.usage_record.auto_scale_up_queue_percent", 70)
- viper.SetDefault("gateway.usage_record.auto_scale_down_queue_percent", 15)
- viper.SetDefault("gateway.usage_record.auto_scale_up_step", 32)
- viper.SetDefault("gateway.usage_record.auto_scale_down_step", 16)
- viper.SetDefault("gateway.usage_record.auto_scale_check_interval_seconds", 3)
- viper.SetDefault("gateway.usage_record.auto_scale_cooldown_seconds", 10)
- viper.SetDefault("gateway.user_group_rate_cache_ttl_seconds", 30)
- viper.SetDefault("gateway.models_list_cache_ttl_seconds", 15)
- // TLS指纹伪装配置(默认关闭,需要账号级别单独启用)
- // 用户消息串行队列默认值
- viper.SetDefault("gateway.user_message_queue.enabled", false)
- viper.SetDefault("gateway.user_message_queue.lock_ttl_ms", 120000)
- viper.SetDefault("gateway.user_message_queue.wait_timeout_ms", 30000)
- viper.SetDefault("gateway.user_message_queue.min_delay_ms", 200)
- viper.SetDefault("gateway.user_message_queue.max_delay_ms", 2000)
- viper.SetDefault("gateway.user_message_queue.cleanup_interval_seconds", 60)
- viper.SetDefault("gateway.tls_fingerprint.enabled", true)
- viper.SetDefault("concurrency.ping_interval", 10)
- // TokenRefresh
- viper.SetDefault("token_refresh.enabled", true)
- viper.SetDefault("token_refresh.check_interval_minutes", 5) // 每5分钟检查一次
- viper.SetDefault("token_refresh.refresh_before_expiry_hours", 0.5) // 提前30分钟刷新(适配Google 1小时token)
- viper.SetDefault("token_refresh.max_retries", 3) // 最多重试3次
- viper.SetDefault("token_refresh.retry_backoff_seconds", 2) // 重试退避基础2秒
- viper.SetDefault("token_refresh.candidate_page_size", 200)
- viper.SetDefault("token_refresh.provider_concurrency", 4)
- viper.SetDefault("token_refresh.provider_qps", 2)
- viper.SetDefault("token_refresh.provider_failure_threshold", 3)
- viper.SetDefault("token_refresh.attempt_timeout_seconds", 15)
- viper.SetDefault("token_refresh.cycle_timeout_seconds", 240)
- // Gemini OAuth - configure via environment variables or config file
- // GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET
- // Default: uses Gemini CLI public credentials (set via environment)
- viper.SetDefault("gemini.oauth.client_id", "")
- viper.SetDefault("gemini.oauth.client_secret", "")
- viper.SetDefault("gemini.oauth.scopes", "")
- viper.SetDefault("gemini.quota.policy", "")
- // Subscription Maintenance (bounded queue + worker pool)
- viper.SetDefault("subscription_maintenance.worker_count", 2)
- viper.SetDefault("subscription_maintenance.queue_size", 1024)
- setEnvReachableDefaults()
- }
- // setEnvReachableDefaults registers zero-valued defaults for keys that are
- // documented in deploy/config.example.yaml but had no default of their own.
- //
- // viper.Unmarshal only decodes the keys returned by AllKeys(), which unions
- // SetDefault keys, config-file keys and explicitly bound BindEnv keys.
- // AutomaticEnv can override a key already in that union, but it never adds one,
- // and the viper_bind_struct escape hatch is compiled out (we build with
- // -tags embed). So a key that lives only in the example file was unreachable by
- // environment variable: the value was read from the process environment and
- // then silently dropped. Deployments driven purely by env — which is what
- // deploy/docker-compose.yml does — got the zero value with no warning.
- //
- // The values below are deliberately zero rather than the documented example
- // values: an absent key already unmarshalled to the zero value, so registering
- // zero keeps behavior identical while making the key addressable from the
- // environment. Any subsystem that wants a richer default still applies it after
- // unmarshal, exactly as before.
- func setEnvReachableDefaults() {
- viper.SetDefault("gateway.forced_codex_instructions_template_file", "")
- viper.SetDefault("gateway.session_idle_timeout_minutes", 0)
- viper.SetDefault("gateway.user_message_queue.mode", "")
- viper.SetDefault("update.proxy_url", "")
- // sticky_escape_enabled is the one exception to the zero-value rule: its
- // effective default is true, applied post-unmarshal via a viper.IsSet guard.
- // Registering false would make IsSet always report true and permanently
- // disable sticky escape, so register the effective default instead. An
- // explicit false in config or env still wins.
- viper.SetDefault("gateway.openai_scheduler.sticky_escape_enabled", true)
- viper.SetDefault("gateway.openai_scheduler.sticky_escape_error_rate", 0.0)
- viper.SetDefault("gateway.openai_scheduler.sticky_escape_ttft_ms", 0)
- // server.trusted_proxies and security.forwarded_client_ip_headers are the
- // other exception: load() distinguishes explicit configuration from absence
- // (issue #4600), and viper.IsSet also reports registered defaults, so a
- // SetDefault would make trusted proxies look permanently configured. Both
- // environment variables are parsed by hand in load() via os.LookupEnv and
- // were never silently dropped; binding them here records that reachability
- // where AllKeys() — and the env-reachability guard — can see it, without
- // affecting IsSet while the variables are absent. BindEnv only errors when
- // called without arguments.
- _ = viper.BindEnv("server.trusted_proxies", "SERVER_TRUSTED_PROXIES")
- _ = viper.BindEnv("security.forwarded_client_ip_headers", "SECURITY_FORWARDED_CLIENT_IP_HEADERS")
- // Third-party login providers. These carry client secrets and are exactly
- // the settings an operator expects to inject via the environment, but every
- // key here was previously unreachable that way.
- for _, provider := range []string{"github_oauth", "google_oauth"} {
- viper.SetDefault(provider+".enabled", false)
- viper.SetDefault(provider+".client_id", "")
- viper.SetDefault(provider+".client_secret", "")
- viper.SetDefault(provider+".authorize_url", "")
- viper.SetDefault(provider+".token_url", "")
- viper.SetDefault(provider+".userinfo_url", "")
- viper.SetDefault(provider+".emails_url", "")
- viper.SetDefault(provider+".scopes", "")
- viper.SetDefault(provider+".redirect_url", "")
- viper.SetDefault(provider+".frontend_redirect_url", "")
- }
- viper.SetDefault("dingtalk_connect.client_id", "")
- viper.SetDefault("dingtalk_connect.client_secret", "")
- viper.SetDefault("dingtalk_connect.internal_corp_id", "")
- viper.SetDefault("dingtalk_connect.redirect_url", "")
- viper.SetDefault("dingtalk_connect.bypass_registration", false)
- viper.SetDefault("dingtalk_connect.username_attribute_key", "")
- viper.SetDefault("dingtalk_connect.enable_attribute_matching", false)
- viper.SetDefault("dingtalk_connect.enable_attribute_sync", false)
- viper.SetDefault("dingtalk_connect.attribute_sync_fields", []string{})
- viper.SetDefault("dingtalk_connect.attribute_sync_overwrite_policy", "")
- viper.SetDefault("dingtalk_connect.sync_display_name", false)
- viper.SetDefault("dingtalk_connect.sync_display_name_attr_key", "")
- viper.SetDefault("dingtalk_connect.sync_display_name_attr_name", "")
- viper.SetDefault("dingtalk_connect.sync_dept", false)
- viper.SetDefault("dingtalk_connect.sync_dept_attr_key", "")
- viper.SetDefault("dingtalk_connect.sync_dept_attr_name", "")
- viper.SetDefault("dingtalk_connect.sync_corp_email", false)
- viper.SetDefault("dingtalk_connect.sync_corp_email_attr_key", "")
- viper.SetDefault("dingtalk_connect.sync_corp_email_attr_name", "")
- }
- func (c *Config) Validate() error {
- forwardedClientIPHeaders, err := NormalizeForwardedClientIPHeaders(c.Security.ForwardedClientIPHeaders)
- if err != nil {
- return fmt.Errorf("security.forwarded_client_ip_headers: %w", err)
- }
- c.Security.ForwardedClientIPHeaders = forwardedClientIPHeaders
- c.SetForwardedClientIPSettings(c.Security.TrustForwardedIPForAPIKeyACL, forwardedClientIPHeaders)
- proxyProbeURLs, err := normalizeProxyProbeURLs(c.Security.ProxyProbe.URLs)
- if err != nil {
- return fmt.Errorf("security.proxy_probe.urls: %w", err)
- }
- c.Security.ProxyProbe.URLs = proxyProbeURLs
- if c.Plugins.MaxUploadBytes <= 0 || c.Plugins.MaxUploadBytes > 1024*1024*1024 {
- return fmt.Errorf("plugins.max_upload_bytes must be between 1 and 1073741824")
- }
- if c.Plugins.MaxUncompressedBytes < c.Plugins.MaxUploadBytes || c.Plugins.MaxUncompressedBytes > 2*1024*1024*1024 {
- return fmt.Errorf("plugins.max_uncompressed_bytes must be between max_upload_bytes and 2147483648")
- }
- if c.Plugins.StartTimeoutSeconds < 1 || c.Plugins.StartTimeoutSeconds > 120 {
- return fmt.Errorf("plugins.start_timeout_seconds must be between 1 and 120")
- }
- if c.Server.ReadHeaderTimeout < 1 || c.Server.ReadHeaderTimeout > 60 {
- return fmt.Errorf("server.read_header_timeout must be between 1 and 60 seconds")
- }
- if c.Server.MaxHeaderBytes < 8*1024 || c.Server.MaxHeaderBytes > 1024*1024 {
- return fmt.Errorf("server.max_header_bytes must be between 8192 and 1048576 bytes")
- }
- if c.Server.IdleTimeout <= 0 {
- return fmt.Errorf("server.idle_timeout must be positive")
- }
- if c.Server.MaxRequestBodySize < 0 {
- return fmt.Errorf("server.max_request_body_size must be non-negative")
- }
- if c.Server.H2C.Enabled {
- if c.Server.H2C.MaxConcurrentStreams == 0 {
- return fmt.Errorf("server.h2c.max_concurrent_streams must be positive")
- }
- if c.Server.H2C.IdleTimeout <= 0 {
- return fmt.Errorf("server.h2c.idle_timeout must be positive")
- }
- if c.Server.H2C.MaxReadFrameSize < 16*1024 || c.Server.H2C.MaxReadFrameSize > 16*1024*1024-1 {
- return fmt.Errorf("server.h2c.max_read_frame_size must be between 16384 and 16777215 bytes")
- }
- if c.Server.H2C.MaxUploadBufferPerConnection < 65535 {
- return fmt.Errorf("server.h2c.max_upload_buffer_per_connection must be at least 65535 bytes")
- }
- if c.Server.H2C.MaxUploadBufferPerStream <= 0 {
- return fmt.Errorf("server.h2c.max_upload_buffer_per_stream must be positive")
- }
- }
- if c.APIKeyAuth.InvalidAbuse.Enabled {
- if c.APIKeyAuth.InvalidAbuse.Threshold < 10 {
- return fmt.Errorf("api_key_auth_cache.invalid_abuse.threshold must be at least 10")
- }
- if c.APIKeyAuth.InvalidAbuse.WindowSeconds < 1 || c.APIKeyAuth.InvalidAbuse.WindowSeconds > 3600 {
- return fmt.Errorf("api_key_auth_cache.invalid_abuse.window_seconds must be between 1 and 3600")
- }
- if c.APIKeyAuth.InvalidAbuse.BlockSeconds < 1 || c.APIKeyAuth.InvalidAbuse.BlockSeconds > 3600 {
- return fmt.Errorf("api_key_auth_cache.invalid_abuse.block_seconds must be between 1 and 3600")
- }
- if c.APIKeyAuth.InvalidAbuse.Capacity < 256 || c.APIKeyAuth.InvalidAbuse.Capacity > 1_000_000 {
- return fmt.Errorf("api_key_auth_cache.invalid_abuse.capacity must be between 256 and 1000000")
- }
- }
- jwtSecret := strings.TrimSpace(c.JWT.Secret)
- if jwtSecret == "" {
- return fmt.Errorf("jwt.secret is required")
- }
- // NOTE: 按 UTF-8 编码后的字节长度计算。
- // 选择 bytes 而不是 rune 计数,确保二进制/随机串的长度语义更接近“熵”而非“字符数”。
- if len([]byte(jwtSecret)) < 32 {
- return fmt.Errorf("jwt.secret must be at least 32 bytes")
- }
- switch c.Log.Level {
- case "debug", "info", "warn", "error":
- case "":
- return fmt.Errorf("log.level is required")
- default:
- return fmt.Errorf("log.level must be one of: debug/info/warn/error")
- }
- switch c.Log.Format {
- case "json", "console":
- case "":
- return fmt.Errorf("log.format is required")
- default:
- return fmt.Errorf("log.format must be one of: json/console")
- }
- switch c.Log.StacktraceLevel {
- case "none", "error", "fatal":
- case "":
- return fmt.Errorf("log.stacktrace_level is required")
- default:
- return fmt.Errorf("log.stacktrace_level must be one of: none/error/fatal")
- }
- if !c.Log.Output.ToStdout && !c.Log.Output.ToFile {
- return fmt.Errorf("log.output.to_stdout and log.output.to_file cannot both be false")
- }
- if c.Log.Rotation.MaxSizeMB <= 0 {
- return fmt.Errorf("log.rotation.max_size_mb must be positive")
- }
- if c.Log.Rotation.MaxBackups < 0 {
- return fmt.Errorf("log.rotation.max_backups must be non-negative")
- }
- if c.Log.Rotation.MaxAgeDays < 0 {
- return fmt.Errorf("log.rotation.max_age_days must be non-negative")
- }
- if c.Log.Sampling.Enabled {
- if c.Log.Sampling.Initial <= 0 {
- return fmt.Errorf("log.sampling.initial must be positive when sampling is enabled")
- }
- if c.Log.Sampling.Thereafter <= 0 {
- return fmt.Errorf("log.sampling.thereafter must be positive when sampling is enabled")
- }
- } else {
- if c.Log.Sampling.Initial < 0 {
- return fmt.Errorf("log.sampling.initial must be non-negative")
- }
- if c.Log.Sampling.Thereafter < 0 {
- return fmt.Errorf("log.sampling.thereafter must be non-negative")
- }
- }
- if c.SubscriptionMaintenance.WorkerCount < 0 {
- return fmt.Errorf("subscription_maintenance.worker_count must be non-negative")
- }
- if c.SubscriptionMaintenance.QueueSize < 0 {
- return fmt.Errorf("subscription_maintenance.queue_size must be non-negative")
- }
- // Gemini OAuth 配置校验:client_id 与 client_secret 必须同时设置或同时留空。
- // 留空时表示使用内置的 Gemini CLI OAuth 客户端(其 client_secret 通过环境变量注入)。
- geminiClientID := strings.TrimSpace(c.Gemini.OAuth.ClientID)
- geminiClientSecret := strings.TrimSpace(c.Gemini.OAuth.ClientSecret)
- if (geminiClientID == "") != (geminiClientSecret == "") {
- return fmt.Errorf("gemini.oauth.client_id and gemini.oauth.client_secret must be both set or both empty")
- }
- if strings.TrimSpace(c.Server.FrontendURL) != "" {
- if err := ValidateAbsoluteHTTPURL(c.Server.FrontendURL); err != nil {
- return fmt.Errorf("server.frontend_url invalid: %w", err)
- }
- u, err := url.Parse(strings.TrimSpace(c.Server.FrontendURL))
- if err != nil {
- return fmt.Errorf("server.frontend_url invalid: %w", err)
- }
- if u.RawQuery != "" || u.ForceQuery {
- return fmt.Errorf("server.frontend_url invalid: must not include query")
- }
- if u.User != nil {
- return fmt.Errorf("server.frontend_url invalid: must not include userinfo")
- }
- warnIfInsecureURL("server.frontend_url", c.Server.FrontendURL)
- }
- if c.WebAuthn.Enabled {
- c.WebAuthn.RPDisplayName = strings.TrimSpace(c.WebAuthn.RPDisplayName)
- c.WebAuthn.RPID = strings.ToLower(strings.TrimSpace(c.WebAuthn.RPID))
- c.WebAuthn.RPOrigins = normalizeStringSlice(c.WebAuthn.RPOrigins)
- if c.WebAuthn.RPDisplayName == "" {
- return fmt.Errorf("webauthn.rp_display_name is required when passkeys are enabled")
- }
- if c.WebAuthn.RPID == "" {
- return fmt.Errorf("webauthn.rp_id is required when passkeys are enabled")
- }
- if strings.Contains(c.WebAuthn.RPID, "://") || strings.ContainsAny(c.WebAuthn.RPID, "/:") {
- return fmt.Errorf("webauthn.rp_id must be a domain without scheme, port, or path")
- }
- if len(c.WebAuthn.RPOrigins) == 0 {
- return fmt.Errorf("webauthn.rp_origins must contain at least one origin when passkeys are enabled")
- }
- for i, origin := range c.WebAuthn.RPOrigins {
- u, err := url.Parse(origin)
- if err != nil || u.Scheme == "" || u.Host == "" {
- return fmt.Errorf("webauthn.rp_origins contains invalid origin %q", origin)
- }
- if u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "" {
- return fmt.Errorf("webauthn.rp_origins entry %q must not include userinfo, path, query, or fragment", origin)
- }
- u.Scheme = strings.ToLower(u.Scheme)
- u.Host = strings.ToLower(u.Host)
- host := strings.ToLower(u.Hostname())
- localDevelopment := host == "localhost" || host == "127.0.0.1" || host == "::1"
- if u.Scheme != "https" && (u.Scheme != "http" || !localDevelopment) {
- return fmt.Errorf("webauthn.rp_origins entry %q must use HTTPS (HTTP is allowed only for localhost)", origin)
- }
- if host != c.WebAuthn.RPID && !strings.HasSuffix(host, "."+c.WebAuthn.RPID) {
- return fmt.Errorf("webauthn.rp_origins entry %q is not within relying party ID %q", origin, c.WebAuthn.RPID)
- }
- c.WebAuthn.RPOrigins[i] = u.Scheme + "://" + u.Host
- }
- }
- if c.JWT.ExpireHour <= 0 {
- return fmt.Errorf("jwt.expire_hour must be positive")
- }
- if c.JWT.ExpireHour > 168 {
- return fmt.Errorf("jwt.expire_hour must be <= 168 (7 days)")
- }
- if c.JWT.ExpireHour > 24 {
- slog.Warn("jwt.expire_hour is high; consider shorter expiration for security", "expire_hour", c.JWT.ExpireHour)
- }
- // JWT Refresh Token配置验证
- if c.JWT.AccessTokenExpireMinutes < 0 {
- return fmt.Errorf("jwt.access_token_expire_minutes must be non-negative")
- }
- if c.JWT.AccessTokenExpireMinutes > 720 {
- slog.Warn("jwt.access_token_expire_minutes is high; consider shorter expiration for security", "access_token_expire_minutes", c.JWT.AccessTokenExpireMinutes)
- }
- if c.JWT.RefreshTokenExpireDays <= 0 {
- return fmt.Errorf("jwt.refresh_token_expire_days must be positive")
- }
- if c.JWT.RefreshTokenExpireDays > 90 {
- slog.Warn("jwt.refresh_token_expire_days is high; consider shorter expiration for security", "refresh_token_expire_days", c.JWT.RefreshTokenExpireDays)
- }
- if c.JWT.RefreshWindowMinutes < 0 {
- return fmt.Errorf("jwt.refresh_window_minutes must be non-negative")
- }
- if c.Security.CSP.Enabled && strings.TrimSpace(c.Security.CSP.Policy) == "" {
- return fmt.Errorf("security.csp.policy is required when CSP is enabled")
- }
- if c.LinuxDo.Enabled {
- if strings.TrimSpace(c.LinuxDo.ClientID) == "" {
- return fmt.Errorf("linuxdo_connect.client_id is required when linuxdo_connect.enabled=true")
- }
- if strings.TrimSpace(c.LinuxDo.AuthorizeURL) == "" {
- return fmt.Errorf("linuxdo_connect.authorize_url is required when linuxdo_connect.enabled=true")
- }
- if strings.TrimSpace(c.LinuxDo.TokenURL) == "" {
- return fmt.Errorf("linuxdo_connect.token_url is required when linuxdo_connect.enabled=true")
- }
- if strings.TrimSpace(c.LinuxDo.UserInfoURL) == "" {
- return fmt.Errorf("linuxdo_connect.userinfo_url is required when linuxdo_connect.enabled=true")
- }
- if strings.TrimSpace(c.LinuxDo.RedirectURL) == "" {
- return fmt.Errorf("linuxdo_connect.redirect_url is required when linuxdo_connect.enabled=true")
- }
- method := strings.ToLower(strings.TrimSpace(c.LinuxDo.TokenAuthMethod))
- switch method {
- case "", "client_secret_post", "client_secret_basic", "none":
- default:
- return fmt.Errorf("linuxdo_connect.token_auth_method must be one of: client_secret_post/client_secret_basic/none")
- }
- if (method == "" || method == "client_secret_post" || method == "client_secret_basic") &&
- strings.TrimSpace(c.LinuxDo.ClientSecret) == "" {
- return fmt.Errorf("linuxdo_connect.client_secret is required when linuxdo_connect.enabled=true and token_auth_method is client_secret_post/client_secret_basic")
- }
- if strings.TrimSpace(c.LinuxDo.FrontendRedirectURL) == "" {
- return fmt.Errorf("linuxdo_connect.frontend_redirect_url is required when linuxdo_connect.enabled=true")
- }
- if err := ValidateAbsoluteHTTPURL(c.LinuxDo.AuthorizeURL); err != nil {
- return fmt.Errorf("linuxdo_connect.authorize_url invalid: %w", err)
- }
- if err := ValidateAbsoluteHTTPURL(c.LinuxDo.TokenURL); err != nil {
- return fmt.Errorf("linuxdo_connect.token_url invalid: %w", err)
- }
- if err := ValidateAbsoluteHTTPURL(c.LinuxDo.UserInfoURL); err != nil {
- return fmt.Errorf("linuxdo_connect.userinfo_url invalid: %w", err)
- }
- if err := ValidateAbsoluteHTTPURL(c.LinuxDo.RedirectURL); err != nil {
- return fmt.Errorf("linuxdo_connect.redirect_url invalid: %w", err)
- }
- if err := ValidateFrontendRedirectURL(c.LinuxDo.FrontendRedirectURL); err != nil {
- return fmt.Errorf("linuxdo_connect.frontend_redirect_url invalid: %w", err)
- }
- warnIfInsecureURL("linuxdo_connect.authorize_url", c.LinuxDo.AuthorizeURL)
- warnIfInsecureURL("linuxdo_connect.token_url", c.LinuxDo.TokenURL)
- warnIfInsecureURL("linuxdo_connect.userinfo_url", c.LinuxDo.UserInfoURL)
- warnIfInsecureURL("linuxdo_connect.redirect_url", c.LinuxDo.RedirectURL)
- warnIfInsecureURL("linuxdo_connect.frontend_redirect_url", c.LinuxDo.FrontendRedirectURL)
- }
- if c.WeChat.Enabled {
- weChat := c.WeChat
- normalizeWeChatConnectConfig(&weChat)
- if weChat.OpenEnabled {
- if strings.TrimSpace(weChat.OpenAppID) == "" {
- return fmt.Errorf("wechat_connect.open_app_id is required when wechat_connect.open_enabled=true")
- }
- if strings.TrimSpace(weChat.OpenAppSecret) == "" {
- return fmt.Errorf("wechat_connect.open_app_secret is required when wechat_connect.open_enabled=true")
- }
- }
- if weChat.MPEnabled {
- if strings.TrimSpace(weChat.MPAppID) == "" {
- return fmt.Errorf("wechat_connect.mp_app_id is required when wechat_connect.mp_enabled=true")
- }
- if strings.TrimSpace(weChat.MPAppSecret) == "" {
- return fmt.Errorf("wechat_connect.mp_app_secret is required when wechat_connect.mp_enabled=true")
- }
- }
- if weChat.MobileEnabled {
- if strings.TrimSpace(weChat.MobileAppID) == "" {
- return fmt.Errorf("wechat_connect.mobile_app_id is required when wechat_connect.mobile_enabled=true")
- }
- if strings.TrimSpace(weChat.MobileAppSecret) == "" {
- return fmt.Errorf("wechat_connect.mobile_app_secret is required when wechat_connect.mobile_enabled=true")
- }
- }
- if v := strings.TrimSpace(weChat.RedirectURL); v != "" {
- if err := ValidateAbsoluteHTTPURL(v); err != nil {
- return fmt.Errorf("wechat_connect.redirect_url invalid: %w", err)
- }
- warnIfInsecureURL("wechat_connect.redirect_url", v)
- }
- if err := ValidateFrontendRedirectURL(weChat.FrontendRedirectURL); err != nil {
- return fmt.Errorf("wechat_connect.frontend_redirect_url invalid: %w", err)
- }
- warnIfInsecureURL("wechat_connect.frontend_redirect_url", weChat.FrontendRedirectURL)
- }
- if c.OIDC.Enabled {
- if strings.TrimSpace(c.OIDC.ClientID) == "" {
- return fmt.Errorf("oidc_connect.client_id is required when oidc_connect.enabled=true")
- }
- if strings.TrimSpace(c.OIDC.IssuerURL) == "" {
- return fmt.Errorf("oidc_connect.issuer_url is required when oidc_connect.enabled=true")
- }
- if strings.TrimSpace(c.OIDC.RedirectURL) == "" {
- return fmt.Errorf("oidc_connect.redirect_url is required when oidc_connect.enabled=true")
- }
- if strings.TrimSpace(c.OIDC.FrontendRedirectURL) == "" {
- return fmt.Errorf("oidc_connect.frontend_redirect_url is required when oidc_connect.enabled=true")
- }
- if !scopeContainsOpenID(c.OIDC.Scopes) {
- return fmt.Errorf("oidc_connect.scopes must contain openid")
- }
- method := strings.ToLower(strings.TrimSpace(c.OIDC.TokenAuthMethod))
- switch method {
- case "", "client_secret_post", "client_secret_basic", "none":
- default:
- return fmt.Errorf("oidc_connect.token_auth_method must be one of: client_secret_post/client_secret_basic/none")
- }
- if (method == "" || method == "client_secret_post" || method == "client_secret_basic") &&
- strings.TrimSpace(c.OIDC.ClientSecret) == "" {
- return fmt.Errorf("oidc_connect.client_secret is required when oidc_connect.enabled=true and token_auth_method is client_secret_post/client_secret_basic")
- }
- if c.OIDC.ClockSkewSeconds < 0 || c.OIDC.ClockSkewSeconds > 600 {
- return fmt.Errorf("oidc_connect.clock_skew_seconds must be between 0 and 600")
- }
- if c.OIDC.ValidateIDToken && strings.TrimSpace(c.OIDC.AllowedSigningAlgs) == "" {
- return fmt.Errorf("oidc_connect.allowed_signing_algs is required when oidc_connect.validate_id_token=true")
- }
- if err := ValidateAbsoluteHTTPURL(c.OIDC.IssuerURL); err != nil {
- return fmt.Errorf("oidc_connect.issuer_url invalid: %w", err)
- }
- if v := strings.TrimSpace(c.OIDC.DiscoveryURL); v != "" {
- if err := ValidateAbsoluteHTTPURL(v); err != nil {
- return fmt.Errorf("oidc_connect.discovery_url invalid: %w", err)
- }
- }
- if v := strings.TrimSpace(c.OIDC.AuthorizeURL); v != "" {
- if err := ValidateAbsoluteHTTPURL(v); err != nil {
- return fmt.Errorf("oidc_connect.authorize_url invalid: %w", err)
- }
- }
- if v := strings.TrimSpace(c.OIDC.TokenURL); v != "" {
- if err := ValidateAbsoluteHTTPURL(v); err != nil {
- return fmt.Errorf("oidc_connect.token_url invalid: %w", err)
- }
- }
- if v := strings.TrimSpace(c.OIDC.UserInfoURL); v != "" {
- if err := ValidateAbsoluteHTTPURL(v); err != nil {
- return fmt.Errorf("oidc_connect.userinfo_url invalid: %w", err)
- }
- }
- if v := strings.TrimSpace(c.OIDC.JWKSURL); v != "" {
- if err := ValidateAbsoluteHTTPURL(v); err != nil {
- return fmt.Errorf("oidc_connect.jwks_url invalid: %w", err)
- }
- }
- if err := ValidateAbsoluteHTTPURL(c.OIDC.RedirectURL); err != nil {
- return fmt.Errorf("oidc_connect.redirect_url invalid: %w", err)
- }
- if err := ValidateFrontendRedirectURL(c.OIDC.FrontendRedirectURL); err != nil {
- return fmt.Errorf("oidc_connect.frontend_redirect_url invalid: %w", err)
- }
- warnIfInsecureURL("oidc_connect.issuer_url", c.OIDC.IssuerURL)
- warnIfInsecureURL("oidc_connect.discovery_url", c.OIDC.DiscoveryURL)
- warnIfInsecureURL("oidc_connect.authorize_url", c.OIDC.AuthorizeURL)
- warnIfInsecureURL("oidc_connect.token_url", c.OIDC.TokenURL)
- warnIfInsecureURL("oidc_connect.userinfo_url", c.OIDC.UserInfoURL)
- warnIfInsecureURL("oidc_connect.jwks_url", c.OIDC.JWKSURL)
- warnIfInsecureURL("oidc_connect.redirect_url", c.OIDC.RedirectURL)
- warnIfInsecureURL("oidc_connect.frontend_redirect_url", c.OIDC.FrontendRedirectURL)
- }
- if c.Billing.CircuitBreaker.Enabled {
- if c.Billing.CircuitBreaker.FailureThreshold <= 0 {
- return fmt.Errorf("billing.circuit_breaker.failure_threshold must be positive")
- }
- if c.Billing.CircuitBreaker.ResetTimeoutSeconds <= 0 {
- return fmt.Errorf("billing.circuit_breaker.reset_timeout_seconds must be positive")
- }
- if c.Billing.CircuitBreaker.HalfOpenRequests <= 0 {
- return fmt.Errorf("billing.circuit_breaker.half_open_requests must be positive")
- }
- }
- if c.Billing.MinimumBalanceReserve < 0 {
- return fmt.Errorf("billing.minimum_balance_reserve must be non-negative")
- }
- if c.Database.MaxOpenConns <= 0 {
- return fmt.Errorf("database.max_open_conns must be positive")
- }
- if c.Database.MaxIdleConns < 0 {
- return fmt.Errorf("database.max_idle_conns must be non-negative")
- }
- if c.Database.MaxIdleConns > c.Database.MaxOpenConns {
- return fmt.Errorf("database.max_idle_conns cannot exceed database.max_open_conns")
- }
- if c.Database.ConnMaxLifetimeMinutes < 0 {
- return fmt.Errorf("database.conn_max_lifetime_minutes must be non-negative")
- }
- if c.Database.ConnMaxIdleTimeMinutes < 0 {
- return fmt.Errorf("database.conn_max_idle_time_minutes must be non-negative")
- }
- if c.Redis.DialTimeoutSeconds <= 0 {
- return fmt.Errorf("redis.dial_timeout_seconds must be positive")
- }
- if c.Redis.ReadTimeoutSeconds <= 0 {
- return fmt.Errorf("redis.read_timeout_seconds must be positive")
- }
- if c.Redis.WriteTimeoutSeconds <= 0 {
- return fmt.Errorf("redis.write_timeout_seconds must be positive")
- }
- if c.Redis.PoolSize <= 0 {
- return fmt.Errorf("redis.pool_size must be positive")
- }
- if c.Redis.MinIdleConns < 0 {
- return fmt.Errorf("redis.min_idle_conns must be non-negative")
- }
- if c.Redis.MinIdleConns > c.Redis.PoolSize {
- return fmt.Errorf("redis.min_idle_conns cannot exceed redis.pool_size")
- }
- if c.BatchImage.QueueEnabled {
- if strings.TrimSpace(c.BatchImage.QueueReadyKey) == "" {
- return fmt.Errorf("batch_image.queue_ready_key must not be empty")
- }
- if strings.TrimSpace(c.BatchImage.QueueDelayedKey) == "" {
- return fmt.Errorf("batch_image.queue_delayed_key must not be empty")
- }
- if strings.TrimSpace(c.BatchImage.QueueActiveKey) == "" {
- return fmt.Errorf("batch_image.queue_active_key must not be empty")
- }
- if strings.TrimSpace(c.BatchImage.InflightKeyPrefix) == "" {
- return fmt.Errorf("batch_image.inflight_key_prefix must not be empty")
- }
- if strings.TrimSpace(c.BatchImage.LockKeyPrefix) == "" {
- return fmt.Errorf("batch_image.lock_key_prefix must not be empty")
- }
- if c.BatchImage.InflightTTLSeconds <= 0 {
- return fmt.Errorf("batch_image.inflight_ttl_seconds must be positive")
- }
- if c.BatchImage.JobLockTTLSeconds <= 0 {
- return fmt.Errorf("batch_image.job_lock_ttl_seconds must be positive")
- }
- if c.BatchImage.StaleActiveAfterSeconds <= 0 {
- return fmt.Errorf("batch_image.stale_active_after_seconds must be positive")
- }
- if c.BatchImage.DelayedMoveLimit <= 0 {
- return fmt.Errorf("batch_image.delayed_move_limit must be positive")
- }
- if c.BatchImage.RecoverLimit <= 0 {
- return fmt.Errorf("batch_image.recover_limit must be positive")
- }
- }
- if c.BatchImage.VertexEnabled {
- if strings.TrimSpace(c.BatchImage.VertexManagedGCSBucket) == "" {
- return fmt.Errorf("batch_image.vertex_managed_gcs_bucket must not be empty when vertex is enabled")
- }
- if strings.Contains(c.BatchImage.VertexManagedGCSBucket, "://") {
- return fmt.Errorf("batch_image.vertex_managed_gcs_bucket must be a bucket name, not a URI")
- }
- if strings.TrimSpace(c.BatchImage.VertexLocation) == "" {
- return fmt.Errorf("batch_image.vertex_location must not be empty when vertex is enabled")
- }
- if strings.TrimSpace(c.BatchImage.VertexManagedGCSPrefix) == "" {
- return fmt.Errorf("batch_image.vertex_managed_gcs_prefix must not be empty when vertex is enabled")
- }
- if !strings.Contains(c.BatchImage.VertexManagedGCSPrefix, "{batch_id}") {
- return fmt.Errorf("batch_image.vertex_managed_gcs_prefix must contain {batch_id}")
- }
- if c.BatchImage.VertexInputRetentionHours <= 0 {
- return fmt.Errorf("batch_image.vertex_input_retention_hours must be positive")
- }
- if c.BatchImage.VertexOutputRetentionHours <= 0 {
- return fmt.Errorf("batch_image.vertex_output_retention_hours must be positive")
- }
- }
- if c.Dashboard.Enabled {
- if c.Dashboard.StatsFreshTTLSeconds <= 0 {
- return fmt.Errorf("dashboard_cache.stats_fresh_ttl_seconds must be positive")
- }
- if c.Dashboard.StatsTTLSeconds <= 0 {
- return fmt.Errorf("dashboard_cache.stats_ttl_seconds must be positive")
- }
- if c.Dashboard.StatsRefreshTimeoutSeconds <= 0 {
- return fmt.Errorf("dashboard_cache.stats_refresh_timeout_seconds must be positive")
- }
- if c.Dashboard.StatsFreshTTLSeconds > c.Dashboard.StatsTTLSeconds {
- return fmt.Errorf("dashboard_cache.stats_fresh_ttl_seconds must be <= dashboard_cache.stats_ttl_seconds")
- }
- } else {
- if c.Dashboard.StatsFreshTTLSeconds < 0 {
- return fmt.Errorf("dashboard_cache.stats_fresh_ttl_seconds must be non-negative")
- }
- if c.Dashboard.StatsTTLSeconds < 0 {
- return fmt.Errorf("dashboard_cache.stats_ttl_seconds must be non-negative")
- }
- if c.Dashboard.StatsRefreshTimeoutSeconds < 0 {
- return fmt.Errorf("dashboard_cache.stats_refresh_timeout_seconds must be non-negative")
- }
- }
- if c.DashboardAgg.Enabled {
- if c.DashboardAgg.IntervalSeconds <= 0 {
- return fmt.Errorf("dashboard_aggregation.interval_seconds must be positive")
- }
- if c.DashboardAgg.LookbackSeconds < 0 {
- return fmt.Errorf("dashboard_aggregation.lookback_seconds must be non-negative")
- }
- if c.DashboardAgg.BackfillMaxDays < 0 {
- return fmt.Errorf("dashboard_aggregation.backfill_max_days must be non-negative")
- }
- if c.DashboardAgg.BackfillEnabled && c.DashboardAgg.BackfillMaxDays == 0 {
- return fmt.Errorf("dashboard_aggregation.backfill_max_days must be positive")
- }
- if c.DashboardAgg.Retention.UsageLogsDays <= 0 {
- return fmt.Errorf("dashboard_aggregation.retention.usage_logs_days must be positive")
- }
- if c.DashboardAgg.Retention.UsageBillingDedupDays <= 0 {
- return fmt.Errorf("dashboard_aggregation.retention.usage_billing_dedup_days must be positive")
- }
- if c.DashboardAgg.Retention.UsageBillingDedupDays < c.DashboardAgg.Retention.UsageLogsDays {
- return fmt.Errorf("dashboard_aggregation.retention.usage_billing_dedup_days must be greater than or equal to usage_logs_days")
- }
- if c.DashboardAgg.Retention.HourlyDays <= 0 {
- return fmt.Errorf("dashboard_aggregation.retention.hourly_days must be positive")
- }
- if c.DashboardAgg.Retention.DailyDays <= 0 {
- return fmt.Errorf("dashboard_aggregation.retention.daily_days must be positive")
- }
- if c.DashboardAgg.RecomputeDays < 0 {
- return fmt.Errorf("dashboard_aggregation.recompute_days must be non-negative")
- }
- } else {
- if c.DashboardAgg.IntervalSeconds < 0 {
- return fmt.Errorf("dashboard_aggregation.interval_seconds must be non-negative")
- }
- if c.DashboardAgg.LookbackSeconds < 0 {
- return fmt.Errorf("dashboard_aggregation.lookback_seconds must be non-negative")
- }
- if c.DashboardAgg.BackfillMaxDays < 0 {
- return fmt.Errorf("dashboard_aggregation.backfill_max_days must be non-negative")
- }
- if c.DashboardAgg.Retention.UsageLogsDays < 0 {
- return fmt.Errorf("dashboard_aggregation.retention.usage_logs_days must be non-negative")
- }
- if c.DashboardAgg.Retention.UsageBillingDedupDays < 0 {
- return fmt.Errorf("dashboard_aggregation.retention.usage_billing_dedup_days must be non-negative")
- }
- if c.DashboardAgg.Retention.UsageBillingDedupDays > 0 &&
- c.DashboardAgg.Retention.UsageLogsDays > 0 &&
- c.DashboardAgg.Retention.UsageBillingDedupDays < c.DashboardAgg.Retention.UsageLogsDays {
- return fmt.Errorf("dashboard_aggregation.retention.usage_billing_dedup_days must be greater than or equal to usage_logs_days")
- }
- if c.DashboardAgg.Retention.HourlyDays < 0 {
- return fmt.Errorf("dashboard_aggregation.retention.hourly_days must be non-negative")
- }
- if c.DashboardAgg.Retention.DailyDays < 0 {
- return fmt.Errorf("dashboard_aggregation.retention.daily_days must be non-negative")
- }
- if c.DashboardAgg.RecomputeDays < 0 {
- return fmt.Errorf("dashboard_aggregation.recompute_days must be non-negative")
- }
- }
- if c.UsageCleanup.Enabled {
- if c.UsageCleanup.MaxRangeDays <= 0 {
- return fmt.Errorf("usage_cleanup.max_range_days must be positive")
- }
- if c.UsageCleanup.BatchSize <= 0 {
- return fmt.Errorf("usage_cleanup.batch_size must be positive")
- }
- if c.UsageCleanup.WorkerIntervalSeconds <= 0 {
- return fmt.Errorf("usage_cleanup.worker_interval_seconds must be positive")
- }
- if c.UsageCleanup.TaskTimeoutSeconds <= 0 {
- return fmt.Errorf("usage_cleanup.task_timeout_seconds must be positive")
- }
- } else {
- if c.UsageCleanup.MaxRangeDays < 0 {
- return fmt.Errorf("usage_cleanup.max_range_days must be non-negative")
- }
- if c.UsageCleanup.BatchSize < 0 {
- return fmt.Errorf("usage_cleanup.batch_size must be non-negative")
- }
- if c.UsageCleanup.WorkerIntervalSeconds < 0 {
- return fmt.Errorf("usage_cleanup.worker_interval_seconds must be non-negative")
- }
- if c.UsageCleanup.TaskTimeoutSeconds < 0 {
- return fmt.Errorf("usage_cleanup.task_timeout_seconds must be non-negative")
- }
- }
- if c.Idempotency.DefaultTTLSeconds <= 0 {
- return fmt.Errorf("idempotency.default_ttl_seconds must be positive")
- }
- if c.Idempotency.SystemOperationTTLSeconds <= 0 {
- return fmt.Errorf("idempotency.system_operation_ttl_seconds must be positive")
- }
- if c.Idempotency.ProcessingTimeoutSeconds <= 0 {
- return fmt.Errorf("idempotency.processing_timeout_seconds must be positive")
- }
- if c.Idempotency.FailedRetryBackoffSeconds <= 0 {
- return fmt.Errorf("idempotency.failed_retry_backoff_seconds must be positive")
- }
- if c.Idempotency.MaxStoredResponseLen <= 0 {
- return fmt.Errorf("idempotency.max_stored_response_len must be positive")
- }
- if c.Idempotency.CleanupIntervalSeconds <= 0 {
- return fmt.Errorf("idempotency.cleanup_interval_seconds must be positive")
- }
- if c.Idempotency.CleanupBatchSize <= 0 {
- return fmt.Errorf("idempotency.cleanup_batch_size must be positive")
- }
- if c.Gateway.MaxBodySize <= 0 {
- return fmt.Errorf("gateway.max_body_size must be positive")
- }
- if c.Gateway.TextMaxBodySize <= 0 || c.Gateway.TextMaxBodySize > c.Gateway.MaxBodySize {
- return fmt.Errorf("gateway.text_max_body_size must be positive and no greater than gateway.max_body_size")
- }
- if c.Gateway.UpstreamResponseReadMaxBytes <= 0 {
- return fmt.Errorf("gateway.upstream_response_read_max_bytes must be positive")
- }
- if c.Gateway.ModelsListReadMaxBytes <= 0 {
- return fmt.Errorf("gateway.models_list_read_max_bytes must be positive")
- }
- if c.Gateway.ProxyProbeResponseReadMaxBytes <= 0 {
- return fmt.Errorf("gateway.proxy_probe_response_read_max_bytes must be positive")
- }
- if c.Gateway.ResponseHeaderTimeout < 0 {
- return fmt.Errorf("gateway.response_header_timeout must be non-negative")
- }
- if c.Gateway.OpenAIResponseHeaderTimeout < 0 {
- return fmt.Errorf("gateway.openai_response_header_timeout must be non-negative")
- }
- if c.Gateway.GrokResponseHeaderTimeout < 0 || c.Gateway.GrokResponseHeaderTimeout > 1800 {
- return fmt.Errorf("gateway.grok_response_header_timeout must be between 0-1800 seconds")
- }
- if c.Gateway.OpenAIFirstOutputTimeoutSeconds < 0 || c.Gateway.OpenAIFirstOutputTimeoutSeconds > 600 ||
- (c.Gateway.OpenAIFirstOutputTimeoutSeconds > 0 && c.Gateway.OpenAIFirstOutputTimeoutSeconds < 30) {
- return fmt.Errorf("gateway.openai_first_output_timeout_seconds must be 0 or between 30-600 seconds")
- }
- if c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds < 0 || c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds > 1800 ||
- (c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds > 0 && c.Gateway.OpenAIHighEffortFirstOutputTimeoutSeconds < 30) {
- return fmt.Errorf("gateway.openai_high_effort_first_output_timeout_seconds must be 0 or between 30-1800 seconds")
- }
- if c.Gateway.Live.MaxSessionDurationSeconds <= 0 {
- c.Gateway.Live.MaxSessionDurationSeconds = 3600
- }
- if strings.TrimSpace(c.Gateway.ConnectionPoolIsolation) != "" {
- switch c.Gateway.ConnectionPoolIsolation {
- case ConnectionPoolIsolationProxy, ConnectionPoolIsolationAccount, ConnectionPoolIsolationAccountProxy:
- default:
- return fmt.Errorf("gateway.connection_pool_isolation must be one of: %s/%s/%s",
- ConnectionPoolIsolationProxy, ConnectionPoolIsolationAccount, ConnectionPoolIsolationAccountProxy)
- }
- }
- if c.Gateway.ImageConcurrency.MaxConcurrentRequests < 0 {
- return fmt.Errorf("gateway.image_concurrency.max_concurrent_requests must be non-negative")
- }
- switch strings.TrimSpace(c.Gateway.ImageConcurrency.OverflowMode) {
- case "", ImageConcurrencyOverflowModeReject, ImageConcurrencyOverflowModeWait:
- default:
- return fmt.Errorf("gateway.image_concurrency.overflow_mode must be one of: %s/%s",
- ImageConcurrencyOverflowModeReject, ImageConcurrencyOverflowModeWait)
- }
- if c.Gateway.ImageConcurrency.WaitTimeoutSeconds < 0 {
- return fmt.Errorf("gateway.image_concurrency.wait_timeout_seconds must be non-negative")
- }
- if c.Gateway.ImageConcurrency.MaxWaitingRequests < 0 {
- return fmt.Errorf("gateway.image_concurrency.max_waiting_requests must be non-negative")
- }
- if c.Gateway.MaxIdleConns <= 0 {
- return fmt.Errorf("gateway.max_idle_conns must be positive")
- }
- if c.Gateway.MaxIdleConnsPerHost <= 0 {
- return fmt.Errorf("gateway.max_idle_conns_per_host must be positive")
- }
- if c.Gateway.MaxConnsPerHost < 0 {
- return fmt.Errorf("gateway.max_conns_per_host must be non-negative")
- }
- if c.Gateway.IdleConnTimeoutSeconds <= 0 {
- return fmt.Errorf("gateway.idle_conn_timeout_seconds must be positive")
- }
- if c.Gateway.IdleConnTimeoutSeconds > 180 {
- slog.Warn("gateway.idle_conn_timeout_seconds is high; consider 60-120 seconds for better connection reuse", "idle_conn_timeout_seconds", c.Gateway.IdleConnTimeoutSeconds)
- }
- if c.Gateway.MaxUpstreamClients <= 0 {
- return fmt.Errorf("gateway.max_upstream_clients must be positive")
- }
- if c.Gateway.ClientIdleTTLSeconds <= 0 {
- return fmt.Errorf("gateway.client_idle_ttl_seconds must be positive")
- }
- if c.Gateway.ConcurrencySlotTTLMinutes <= 0 {
- return fmt.Errorf("gateway.concurrency_slot_ttl_minutes must be positive")
- }
- if c.Gateway.StreamDataIntervalTimeout < 0 {
- return fmt.Errorf("gateway.stream_data_interval_timeout must be non-negative")
- }
- if c.Gateway.StreamDataIntervalTimeout != 0 &&
- (c.Gateway.StreamDataIntervalTimeout < 30 || c.Gateway.StreamDataIntervalTimeout > 300) {
- return fmt.Errorf("gateway.stream_data_interval_timeout must be 0 or between 30-300 seconds")
- }
- if c.Gateway.StreamKeepaliveInterval < 0 {
- return fmt.Errorf("gateway.stream_keepalive_interval must be non-negative")
- }
- if c.Gateway.StreamKeepaliveInterval != 0 &&
- (c.Gateway.StreamKeepaliveInterval < 5 || c.Gateway.StreamKeepaliveInterval > 30) {
- return fmt.Errorf("gateway.stream_keepalive_interval must be 0 or between 5-30 seconds")
- }
- if c.Gateway.ImageStreamDataIntervalTimeout < 0 {
- return fmt.Errorf("gateway.image_stream_data_interval_timeout must be non-negative")
- }
- if c.Gateway.ImageStreamDataIntervalTimeout != 0 &&
- (c.Gateway.ImageStreamDataIntervalTimeout < 60 || c.Gateway.ImageStreamDataIntervalTimeout > 1800) {
- return fmt.Errorf("gateway.image_stream_data_interval_timeout must be 0 or between 60-1800 seconds")
- }
- if c.Gateway.ImageStreamKeepaliveInterval < 0 {
- return fmt.Errorf("gateway.image_stream_keepalive_interval must be non-negative")
- }
- if c.Gateway.ImageStreamKeepaliveInterval != 0 &&
- (c.Gateway.ImageStreamKeepaliveInterval < 5 || c.Gateway.ImageStreamKeepaliveInterval > 60) {
- return fmt.Errorf("gateway.image_stream_keepalive_interval must be 0 or between 5-60 seconds")
- }
- if c.Gateway.ImageNonstreamKeepaliveInterval < 0 {
- return fmt.Errorf("gateway.image_nonstream_keepalive_interval must be non-negative")
- }
- if c.Gateway.ImageNonstreamKeepaliveInterval != 0 &&
- (c.Gateway.ImageNonstreamKeepaliveInterval < 5 || c.Gateway.ImageNonstreamKeepaliveInterval > 60) {
- return fmt.Errorf("gateway.image_nonstream_keepalive_interval must be 0 or between 5-60 seconds")
- }
- // 兼容旧键 sticky_previous_response_ttl_seconds
- if c.Gateway.OpenAIWS.StickyResponseIDTTLSeconds <= 0 && c.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds > 0 {
- c.Gateway.OpenAIWS.StickyResponseIDTTLSeconds = c.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds
- }
- if c.Gateway.OpenAIWS.MaxConnsPerAccount <= 0 {
- return fmt.Errorf("gateway.openai_ws.max_conns_per_account must be positive")
- }
- if c.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds <= 0 {
- return fmt.Errorf("gateway.openai_ws.client_first_message_timeout_seconds must be positive")
- }
- if c.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds < 0 {
- return fmt.Errorf("gateway.openai_ws.ingress_inter_turn_idle_timeout_seconds must be non-negative")
- }
- if c.Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey < 0 {
- return fmt.Errorf("gateway.openai_ws.max_ingress_connections_per_api_key must be non-negative")
- }
- if c.Gateway.OpenAIWS.MinIdlePerAccount < 0 {
- return fmt.Errorf("gateway.openai_ws.min_idle_per_account must be non-negative")
- }
- if c.Gateway.OpenAIWS.MaxIdlePerAccount < 0 {
- return fmt.Errorf("gateway.openai_ws.max_idle_per_account must be non-negative")
- }
- if c.Gateway.OpenAIWS.MinIdlePerAccount > c.Gateway.OpenAIWS.MaxIdlePerAccount {
- return fmt.Errorf("gateway.openai_ws.min_idle_per_account must be <= max_idle_per_account")
- }
- if c.Gateway.OpenAIWS.MaxIdlePerAccount > c.Gateway.OpenAIWS.MaxConnsPerAccount {
- return fmt.Errorf("gateway.openai_ws.max_idle_per_account must be <= max_conns_per_account")
- }
- if c.Gateway.OpenAIWS.OAuthMaxConnsFactor <= 0 {
- return fmt.Errorf("gateway.openai_ws.oauth_max_conns_factor must be positive")
- }
- if c.Gateway.OpenAIWS.APIKeyMaxConnsFactor <= 0 {
- return fmt.Errorf("gateway.openai_ws.apikey_max_conns_factor must be positive")
- }
- if c.Gateway.OpenAIWS.DialTimeoutSeconds <= 0 {
- return fmt.Errorf("gateway.openai_ws.dial_timeout_seconds must be positive")
- }
- if c.Gateway.OpenAIWS.ReadTimeoutSeconds <= 0 {
- return fmt.Errorf("gateway.openai_ws.read_timeout_seconds must be positive")
- }
- if c.Gateway.OpenAIWS.WriteTimeoutSeconds <= 0 {
- return fmt.Errorf("gateway.openai_ws.write_timeout_seconds must be positive")
- }
- if c.Gateway.OpenAIWS.PoolTargetUtilization <= 0 || c.Gateway.OpenAIWS.PoolTargetUtilization > 1 {
- return fmt.Errorf("gateway.openai_ws.pool_target_utilization must be within (0,1]")
- }
- if c.Gateway.OpenAIWS.QueueLimitPerConn <= 0 {
- return fmt.Errorf("gateway.openai_ws.queue_limit_per_conn must be positive")
- }
- if c.Gateway.OpenAIWS.EventFlushBatchSize <= 0 {
- return fmt.Errorf("gateway.openai_ws.event_flush_batch_size must be positive")
- }
- if c.Gateway.OpenAIWS.EventFlushIntervalMS < 0 {
- return fmt.Errorf("gateway.openai_ws.event_flush_interval_ms must be non-negative")
- }
- if c.Gateway.OpenAIWS.PrewarmCooldownMS < 0 {
- return fmt.Errorf("gateway.openai_ws.prewarm_cooldown_ms must be non-negative")
- }
- if c.Gateway.OpenAIWS.ClientReadLimitBytes <= 0 {
- return fmt.Errorf("gateway.openai_ws.client_read_limit_bytes must be positive")
- }
- if c.Gateway.OpenAIWS.HTTPBridgeThresholdBytes < 0 {
- return fmt.Errorf("gateway.openai_ws.http_bridge_threshold_bytes must be non-negative")
- }
- if c.Gateway.OpenAIWS.HTTPBridgeEnabled && c.Gateway.OpenAIWS.HTTPBridgeThresholdBytes == 0 {
- return fmt.Errorf("gateway.openai_ws.http_bridge_threshold_bytes must be positive when http_bridge_enabled is true")
- }
- if c.Gateway.OpenAIWS.FallbackCooldownSeconds < 0 {
- return fmt.Errorf("gateway.openai_ws.fallback_cooldown_seconds must be non-negative")
- }
- if c.Gateway.OpenAIWS.RetryBackoffInitialMS < 0 {
- return fmt.Errorf("gateway.openai_ws.retry_backoff_initial_ms must be non-negative")
- }
- if c.Gateway.OpenAIWS.RetryBackoffMaxMS < 0 {
- return fmt.Errorf("gateway.openai_ws.retry_backoff_max_ms must be non-negative")
- }
- if c.Gateway.OpenAIWS.RetryBackoffInitialMS > 0 && c.Gateway.OpenAIWS.RetryBackoffMaxMS > 0 &&
- c.Gateway.OpenAIWS.RetryBackoffMaxMS < c.Gateway.OpenAIWS.RetryBackoffInitialMS {
- return fmt.Errorf("gateway.openai_ws.retry_backoff_max_ms must be >= retry_backoff_initial_ms")
- }
- if c.Gateway.OpenAIWS.RetryJitterRatio < 0 || c.Gateway.OpenAIWS.RetryJitterRatio > 1 {
- return fmt.Errorf("gateway.openai_ws.retry_jitter_ratio must be within [0,1]")
- }
- if c.Gateway.OpenAIWS.RetryTotalBudgetMS < 0 {
- return fmt.Errorf("gateway.openai_ws.retry_total_budget_ms must be non-negative")
- }
- if mode := strings.ToLower(strings.TrimSpace(c.Gateway.OpenAIWS.IngressModeDefault)); mode != "" {
- switch mode {
- case "off", "ctx_pool", "passthrough", "http_bridge":
- case "shared", "dedicated":
- slog.Warn("gateway.openai_ws.ingress_mode_default is deprecated, treating as ctx_pool; please update to off|ctx_pool|passthrough|http_bridge", "value", mode)
- default:
- return fmt.Errorf("gateway.openai_ws.ingress_mode_default must be one of off|ctx_pool|passthrough|http_bridge")
- }
- }
- if mode := strings.ToLower(strings.TrimSpace(c.Gateway.OpenAIWS.StoreDisabledConnMode)); mode != "" {
- switch mode {
- case "strict", "adaptive", "off":
- default:
- return fmt.Errorf("gateway.openai_ws.store_disabled_conn_mode must be one of strict|adaptive|off")
- }
- }
- if c.Gateway.OpenAIWS.PayloadLogSampleRate < 0 || c.Gateway.OpenAIWS.PayloadLogSampleRate > 1 {
- return fmt.Errorf("gateway.openai_ws.payload_log_sample_rate must be within [0,1]")
- }
- if c.Gateway.OpenAIWS.LBTopK <= 0 {
- return fmt.Errorf("gateway.openai_ws.lb_top_k must be positive")
- }
- if c.Gateway.OpenAIWS.StickySessionTTLSeconds <= 0 {
- return fmt.Errorf("gateway.openai_ws.sticky_session_ttl_seconds must be positive")
- }
- if c.Gateway.OpenAIWS.StickyResponseIDTTLSeconds <= 0 {
- return fmt.Errorf("gateway.openai_ws.sticky_response_id_ttl_seconds must be positive")
- }
- if c.Gateway.OpenAIWS.StickyPreviousResponseTTLSeconds < 0 {
- return fmt.Errorf("gateway.openai_ws.sticky_previous_response_ttl_seconds must be non-negative")
- }
- if c.Gateway.OpenAIHTTP2.FallbackErrorThreshold < 0 {
- return fmt.Errorf("gateway.openai_http2.fallback_error_threshold must be non-negative")
- }
- if c.Gateway.OpenAIHTTP2.FallbackWindowSeconds < 0 {
- return fmt.Errorf("gateway.openai_http2.fallback_window_seconds must be non-negative")
- }
- if c.Gateway.OpenAIHTTP2.FallbackTTLSeconds < 0 {
- return fmt.Errorf("gateway.openai_http2.fallback_ttl_seconds must be non-negative")
- }
- if c.Gateway.OpenAIProxyStreamCircuit.FailureThreshold < 0 {
- return fmt.Errorf("gateway.openai_proxy_stream_circuit.failure_threshold must be non-negative")
- }
- if c.Gateway.OpenAIProxyStreamCircuit.WindowSeconds < 0 {
- return fmt.Errorf("gateway.openai_proxy_stream_circuit.window_seconds must be non-negative")
- }
- if c.Gateway.OpenAIProxyStreamCircuit.TTLSeconds < 0 {
- return fmt.Errorf("gateway.openai_proxy_stream_circuit.ttl_seconds must be non-negative")
- }
- weights := c.Gateway.OpenAIWS.SchedulerScoreWeights
- for _, weight := range []float64{
- weights.Priority, weights.Load, weights.Queue, weights.ErrorRate, weights.TTFT,
- weights.Reset, weights.QuotaHeadroom, weights.UpstreamCost,
- weights.PreviousResponse, weights.SessionSticky,
- } {
- if weight < 0 || math.IsNaN(weight) || math.IsInf(weight, 0) {
- return fmt.Errorf("gateway.openai_ws.scheduler_score_weights.* must be non-negative and finite")
- }
- }
- weightSum := weights.BaseWeightSum()
- if weightSum <= 0 {
- return fmt.Errorf("gateway.openai_ws.scheduler_score_weights must not all be zero")
- }
- if math.IsNaN(weightSum) || math.IsInf(weightSum, 0) {
- return fmt.Errorf("gateway.openai_ws.scheduler_score_weights base-weight sum must be finite")
- }
- if totalWeightSum := weights.TotalWeightSum(); math.IsNaN(totalWeightSum) || math.IsInf(totalWeightSum, 0) {
- return fmt.Errorf("gateway.openai_ws.scheduler_score_weights total-weight sum must be finite")
- }
- if c.Gateway.OpenAIScheduler.StickyEscapeTTFTMs <= 0 {
- return fmt.Errorf("gateway.openai_scheduler.sticky_escape_ttft_ms must be positive")
- }
- if c.Gateway.OpenAIScheduler.StickyEscapeErrorRate < 0 || c.Gateway.OpenAIScheduler.StickyEscapeErrorRate > 1 {
- return fmt.Errorf("gateway.openai_scheduler.sticky_escape_error_rate must be between 0 and 1")
- }
- if c.Gateway.MaxLineSize < 0 {
- return fmt.Errorf("gateway.max_line_size must be non-negative")
- }
- if c.Gateway.MaxLineSize != 0 && c.Gateway.MaxLineSize < 1024*1024 {
- return fmt.Errorf("gateway.max_line_size must be at least 1MB")
- }
- if c.Gateway.UsageRecord.WorkerCount <= 0 {
- return fmt.Errorf("gateway.usage_record.worker_count must be positive")
- }
- if c.Gateway.UsageRecord.QueueSize <= 0 {
- return fmt.Errorf("gateway.usage_record.queue_size must be positive")
- }
- if c.Gateway.UsageRecord.TaskTimeoutSeconds <= 0 {
- return fmt.Errorf("gateway.usage_record.task_timeout_seconds must be positive")
- }
- switch strings.ToLower(strings.TrimSpace(c.Gateway.UsageRecord.OverflowPolicy)) {
- case UsageRecordOverflowPolicyDrop, UsageRecordOverflowPolicySample, UsageRecordOverflowPolicySync:
- default:
- return fmt.Errorf("gateway.usage_record.overflow_policy must be one of: %s/%s/%s",
- UsageRecordOverflowPolicyDrop, UsageRecordOverflowPolicySample, UsageRecordOverflowPolicySync)
- }
- if c.Gateway.UsageRecord.OverflowSamplePercent < 0 || c.Gateway.UsageRecord.OverflowSamplePercent > 100 {
- return fmt.Errorf("gateway.usage_record.overflow_sample_percent must be between 0-100")
- }
- if strings.EqualFold(strings.TrimSpace(c.Gateway.UsageRecord.OverflowPolicy), UsageRecordOverflowPolicySample) &&
- c.Gateway.UsageRecord.OverflowSamplePercent <= 0 {
- return fmt.Errorf("gateway.usage_record.overflow_sample_percent must be positive when overflow_policy=sample")
- }
- if c.Gateway.UsageRecord.AutoScaleEnabled {
- if c.Gateway.UsageRecord.AutoScaleMinWorkers <= 0 {
- return fmt.Errorf("gateway.usage_record.auto_scale_min_workers must be positive")
- }
- if c.Gateway.UsageRecord.AutoScaleMaxWorkers <= 0 {
- return fmt.Errorf("gateway.usage_record.auto_scale_max_workers must be positive")
- }
- if c.Gateway.UsageRecord.AutoScaleMaxWorkers < c.Gateway.UsageRecord.AutoScaleMinWorkers {
- return fmt.Errorf("gateway.usage_record.auto_scale_max_workers must be >= auto_scale_min_workers")
- }
- if c.Gateway.UsageRecord.WorkerCount < c.Gateway.UsageRecord.AutoScaleMinWorkers ||
- c.Gateway.UsageRecord.WorkerCount > c.Gateway.UsageRecord.AutoScaleMaxWorkers {
- return fmt.Errorf("gateway.usage_record.worker_count must be between auto_scale_min_workers and auto_scale_max_workers")
- }
- if c.Gateway.UsageRecord.AutoScaleUpQueuePercent <= 0 || c.Gateway.UsageRecord.AutoScaleUpQueuePercent > 100 {
- return fmt.Errorf("gateway.usage_record.auto_scale_up_queue_percent must be between 1-100")
- }
- if c.Gateway.UsageRecord.AutoScaleDownQueuePercent < 0 || c.Gateway.UsageRecord.AutoScaleDownQueuePercent >= 100 {
- return fmt.Errorf("gateway.usage_record.auto_scale_down_queue_percent must be between 0-99")
- }
- if c.Gateway.UsageRecord.AutoScaleDownQueuePercent >= c.Gateway.UsageRecord.AutoScaleUpQueuePercent {
- return fmt.Errorf("gateway.usage_record.auto_scale_down_queue_percent must be less than auto_scale_up_queue_percent")
- }
- if c.Gateway.UsageRecord.AutoScaleUpStep <= 0 {
- return fmt.Errorf("gateway.usage_record.auto_scale_up_step must be positive")
- }
- if c.Gateway.UsageRecord.AutoScaleDownStep <= 0 {
- return fmt.Errorf("gateway.usage_record.auto_scale_down_step must be positive")
- }
- if c.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds <= 0 {
- return fmt.Errorf("gateway.usage_record.auto_scale_check_interval_seconds must be positive")
- }
- if c.Gateway.UsageRecord.AutoScaleCooldownSeconds < 0 {
- return fmt.Errorf("gateway.usage_record.auto_scale_cooldown_seconds must be non-negative")
- }
- }
- if c.Gateway.UserGroupRateCacheTTLSeconds <= 0 {
- return fmt.Errorf("gateway.user_group_rate_cache_ttl_seconds must be positive")
- }
- if c.Gateway.ModelsListCacheTTLSeconds < 10 || c.Gateway.ModelsListCacheTTLSeconds > 30 {
- return fmt.Errorf("gateway.models_list_cache_ttl_seconds must be between 10-30")
- }
- if c.Gateway.Scheduling.StickySessionMaxWaiting <= 0 {
- return fmt.Errorf("gateway.scheduling.sticky_session_max_waiting must be positive")
- }
- if c.Gateway.Scheduling.StickySessionWaitTimeout <= 0 {
- return fmt.Errorf("gateway.scheduling.sticky_session_wait_timeout must be positive")
- }
- if c.Gateway.Scheduling.FallbackWaitTimeout <= 0 {
- return fmt.Errorf("gateway.scheduling.fallback_wait_timeout must be positive")
- }
- if c.Gateway.Scheduling.FallbackMaxWaiting <= 0 {
- return fmt.Errorf("gateway.scheduling.fallback_max_waiting must be positive")
- }
- if c.Gateway.Scheduling.LoadBatchCacheTTLMS < 0 {
- return fmt.Errorf("gateway.scheduling.load_batch_cache_ttl_ms must be non-negative")
- }
- if c.Gateway.Scheduling.SnapshotMGetChunkSize <= 0 {
- return fmt.Errorf("gateway.scheduling.snapshot_mget_chunk_size must be positive")
- }
- if c.Gateway.Scheduling.SnapshotWriteChunkSize <= 0 {
- return fmt.Errorf("gateway.scheduling.snapshot_write_chunk_size must be positive")
- }
- if c.Gateway.Scheduling.SlotCleanupInterval < 0 {
- return fmt.Errorf("gateway.scheduling.slot_cleanup_interval must be non-negative")
- }
- if c.Gateway.Scheduling.DbFallbackTimeoutSeconds < 0 {
- return fmt.Errorf("gateway.scheduling.db_fallback_timeout_seconds must be non-negative")
- }
- if c.Gateway.Scheduling.DbFallbackMaxQPS < 0 {
- return fmt.Errorf("gateway.scheduling.db_fallback_max_qps must be non-negative")
- }
- if c.Gateway.Scheduling.OutboxPollIntervalSeconds <= 0 {
- return fmt.Errorf("gateway.scheduling.outbox_poll_interval_seconds must be positive")
- }
- if c.Gateway.Scheduling.OutboxLagWarnSeconds < 0 {
- return fmt.Errorf("gateway.scheduling.outbox_lag_warn_seconds must be non-negative")
- }
- if c.Gateway.Scheduling.OutboxLagRebuildSeconds < 0 {
- return fmt.Errorf("gateway.scheduling.outbox_lag_rebuild_seconds must be non-negative")
- }
- if c.Gateway.Scheduling.OutboxLagRebuildFailures <= 0 {
- return fmt.Errorf("gateway.scheduling.outbox_lag_rebuild_failures must be positive")
- }
- if c.Gateway.Scheduling.OutboxBacklogRebuildRows < 0 {
- return fmt.Errorf("gateway.scheduling.outbox_backlog_rebuild_rows must be non-negative")
- }
- if c.Gateway.Scheduling.FullRebuildIntervalSeconds < 0 {
- return fmt.Errorf("gateway.scheduling.full_rebuild_interval_seconds must be non-negative")
- }
- if c.Gateway.Scheduling.OutboxLagWarnSeconds > 0 &&
- c.Gateway.Scheduling.OutboxLagRebuildSeconds > 0 &&
- c.Gateway.Scheduling.OutboxLagRebuildSeconds < c.Gateway.Scheduling.OutboxLagWarnSeconds {
- return fmt.Errorf("gateway.scheduling.outbox_lag_rebuild_seconds must be >= outbox_lag_warn_seconds")
- }
- if c.Ops.MetricsCollectorCache.TTL < 0 {
- return fmt.Errorf("ops.metrics_collector_cache.ttl must be non-negative")
- }
- if c.Ops.Cleanup.ErrorLogRetentionDays < 0 {
- return fmt.Errorf("ops.cleanup.error_log_retention_days must be non-negative")
- }
- if c.Ops.Cleanup.Enabled && c.Ops.Cleanup.SystemLogRetentionDays <= 0 {
- return fmt.Errorf("ops.cleanup.system_log_retention_days must be positive when ops cleanup is enabled")
- }
- if c.Ops.Cleanup.MinuteMetricsRetentionDays < 0 {
- return fmt.Errorf("ops.cleanup.minute_metrics_retention_days must be non-negative")
- }
- if c.Ops.Cleanup.HourlyMetricsRetentionDays < 0 {
- return fmt.Errorf("ops.cleanup.hourly_metrics_retention_days must be non-negative")
- }
- if c.Ops.Cleanup.Enabled && strings.TrimSpace(c.Ops.Cleanup.Schedule) == "" {
- return fmt.Errorf("ops.cleanup.schedule is required when ops.cleanup.enabled=true")
- }
- if c.Concurrency.PingInterval < 5 || c.Concurrency.PingInterval > 30 {
- return fmt.Errorf("concurrency.ping_interval must be between 5-30 seconds")
- }
- if c.Gateway.Grok.FreeQuotaSoftGateEnabled {
- if c.Gateway.Grok.FreeQuotaTokenLimit <= 0 {
- return fmt.Errorf("gateway.grok.free_quota_token_limit must be positive")
- }
- if c.Gateway.Grok.FreeQuotaSoftGatePercent < 1 || c.Gateway.Grok.FreeQuotaSoftGatePercent > 100 {
- return fmt.Errorf("gateway.grok.free_quota_soft_gate_percent must be between 1 and 100")
- }
- if c.Gateway.Grok.FreeQuotaWindowHours <= 0 {
- return fmt.Errorf("gateway.grok.free_quota_window_hours must be positive")
- }
- }
- if c.Gateway.Grok.FreeQuotaStatsCacheSeconds < 0 {
- return fmt.Errorf("gateway.grok.free_quota_stats_cache_seconds must be non-negative")
- }
- if err := ValidateDingTalkConfig(c.DingTalk); err != nil {
- return fmt.Errorf("dingtalk_connect: %w", err)
- }
- return nil
- }
- func normalizeStringSlice(values []string) []string {
- if len(values) == 0 {
- return values
- }
- normalized := make([]string, 0, len(values))
- for _, v := range values {
- trimmed := strings.TrimSpace(v)
- if trimmed == "" {
- continue
- }
- normalized = append(normalized, trimmed)
- }
- return normalized
- }
- func isWeakJWTSecret(secret string) bool {
- lower := strings.ToLower(strings.TrimSpace(secret))
- if lower == "" {
- return true
- }
- weak := map[string]struct{}{
- "change-me-in-production": {},
- "changeme": {},
- "secret": {},
- "password": {},
- "123456": {},
- "12345678": {},
- "admin": {},
- "jwt-secret": {},
- }
- _, exists := weak[lower]
- return exists
- }
- func generateJWTSecret(byteLength int) (string, error) {
- if byteLength <= 0 {
- byteLength = 32
- }
- buf := make([]byte, byteLength)
- if _, err := rand.Read(buf); err != nil {
- return "", err
- }
- return hex.EncodeToString(buf), nil
- }
- // GetServerAddress returns the server address (host:port) from config file or environment variable.
- // This is a lightweight function that can be used before full config validation,
- // such as during setup wizard startup.
- // Priority: environment variables > config file > defaults.
- func GetServerAddress() string {
- v := viper.New()
- v.SetConfigName("config")
- v.SetConfigType("yaml")
- configureConfigSource(v.SetConfigFile, v.AddConfigPath)
- // Support SERVER_HOST and SERVER_PORT environment variables
- v.AutomaticEnv()
- v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
- v.SetDefault("server.host", "0.0.0.0")
- v.SetDefault("server.port", 8080)
- // Try to read config file (ignore errors if not found)
- _ = v.ReadInConfig()
- host := v.GetString("server.host")
- port := v.GetInt("server.port")
- return fmt.Sprintf("%s:%d", host, port)
- }
- // ValidateAbsoluteHTTPURL 验证是否为有效的绝对 HTTP(S) URL
- func ValidateAbsoluteHTTPURL(raw string) error {
- raw = strings.TrimSpace(raw)
- if raw == "" {
- return fmt.Errorf("empty url")
- }
- u, err := url.Parse(raw)
- if err != nil {
- return err
- }
- if !u.IsAbs() {
- return fmt.Errorf("must be absolute")
- }
- if !isHTTPScheme(u.Scheme) {
- return fmt.Errorf("unsupported scheme: %s", u.Scheme)
- }
- if strings.TrimSpace(u.Host) == "" {
- return fmt.Errorf("missing host")
- }
- if u.Fragment != "" {
- return fmt.Errorf("must not include fragment")
- }
- return nil
- }
- // ValidateFrontendRedirectURL 验证前端重定向 URL(可以是绝对 URL 或相对路径)
- func ValidateFrontendRedirectURL(raw string) error {
- raw = strings.TrimSpace(raw)
- if raw == "" {
- return fmt.Errorf("empty url")
- }
- if strings.ContainsAny(raw, "\r\n") {
- return fmt.Errorf("contains invalid characters")
- }
- if strings.HasPrefix(raw, "/") {
- if strings.HasPrefix(raw, "//") {
- return fmt.Errorf("must not start with //")
- }
- return nil
- }
- u, err := url.Parse(raw)
- if err != nil {
- return err
- }
- if !u.IsAbs() {
- return fmt.Errorf("must be absolute http(s) url or relative path")
- }
- if !isHTTPScheme(u.Scheme) {
- return fmt.Errorf("unsupported scheme: %s", u.Scheme)
- }
- if strings.TrimSpace(u.Host) == "" {
- return fmt.Errorf("missing host")
- }
- if u.Fragment != "" {
- return fmt.Errorf("must not include fragment")
- }
- return nil
- }
- func scopeContainsOpenID(scopes string) bool {
- for _, scope := range strings.Fields(strings.ToLower(strings.TrimSpace(scopes))) {
- if scope == "openid" {
- return true
- }
- }
- return false
- }
- // isHTTPScheme 检查是否为 HTTP 或 HTTPS 协议
- func isHTTPScheme(scheme string) bool {
- return strings.EqualFold(scheme, "http") || strings.EqualFold(scheme, "https")
- }
- func warnIfInsecureURL(field, raw string) {
- u, err := url.Parse(strings.TrimSpace(raw))
- if err != nil {
- return
- }
- if strings.EqualFold(u.Scheme, "http") {
- slog.Warn("url uses http scheme; use https in production to avoid token leakage", "field", field)
- }
- }
|