gateway_handler.go 98 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597
  1. package handler
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "math"
  9. "net/http"
  10. "strconv"
  11. "strings"
  12. "sync/atomic"
  13. "time"
  14. "github.com/Wei-Shaw/sub2api/internal/config"
  15. "github.com/Wei-Shaw/sub2api/internal/domain"
  16. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  17. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  18. "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
  19. pkgerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  20. "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
  21. "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
  22. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  23. "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
  24. "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
  25. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  26. "github.com/Wei-Shaw/sub2api/internal/securityaudit"
  27. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  28. "github.com/Wei-Shaw/sub2api/internal/service"
  29. "github.com/gin-gonic/gin"
  30. "go.uber.org/zap"
  31. )
  32. const gatewayCompatibilityMetricsLogInterval = 1024
  33. var gatewayCompatibilityMetricsLogCounter atomic.Uint64
  34. // GatewayHandler handles API gateway requests
  35. type GatewayHandler struct {
  36. gatewayService *service.GatewayService
  37. openAIGatewayService *service.OpenAIGatewayService
  38. geminiCompatService *service.GeminiMessagesCompatService
  39. antigravityGatewayService *service.AntigravityGatewayService
  40. userService *service.UserService
  41. billingCacheService *service.BillingCacheService
  42. usageService *service.UsageService
  43. apiKeyService *service.APIKeyService
  44. usageRecordWorkerPool *service.UsageRecordWorkerPool
  45. errorPassthroughService *service.ErrorPassthroughService
  46. contentModerationService *service.ContentModerationService
  47. securityPolicyService *service.SecurityPolicyService
  48. securityAuditCoordinator *securityaudit.Coordinator
  49. concurrencyHelper *ConcurrencyHelper
  50. userMsgQueueHelper *UserMsgQueueHelper
  51. maxAccountSwitches int
  52. maxAccountSwitchesGemini int
  53. cfg *config.Config
  54. settingService *service.SettingService
  55. }
  56. // NewGatewayHandler creates a new GatewayHandler
  57. func NewGatewayHandler(
  58. gatewayService *service.GatewayService,
  59. openAIGatewayService *service.OpenAIGatewayService,
  60. geminiCompatService *service.GeminiMessagesCompatService,
  61. antigravityGatewayService *service.AntigravityGatewayService,
  62. userService *service.UserService,
  63. concurrencyService *service.ConcurrencyService,
  64. billingCacheService *service.BillingCacheService,
  65. usageService *service.UsageService,
  66. apiKeyService *service.APIKeyService,
  67. usageRecordWorkerPool *service.UsageRecordWorkerPool,
  68. errorPassthroughService *service.ErrorPassthroughService,
  69. contentModerationService *service.ContentModerationService,
  70. userMsgQueueService *service.UserMessageQueueService,
  71. cfg *config.Config,
  72. settingService *service.SettingService,
  73. ) *GatewayHandler {
  74. pingInterval := time.Duration(0)
  75. maxAccountSwitches := 10
  76. maxAccountSwitchesGemini := 3
  77. if cfg != nil {
  78. pingInterval = time.Duration(cfg.Concurrency.PingInterval) * time.Second
  79. if cfg.Gateway.MaxAccountSwitches > 0 {
  80. maxAccountSwitches = cfg.Gateway.MaxAccountSwitches
  81. }
  82. if cfg.Gateway.MaxAccountSwitchesGemini > 0 {
  83. maxAccountSwitchesGemini = cfg.Gateway.MaxAccountSwitchesGemini
  84. }
  85. }
  86. // 初始化用户消息串行队列 helper
  87. var umqHelper *UserMsgQueueHelper
  88. if userMsgQueueService != nil && cfg != nil {
  89. umqHelper = NewUserMsgQueueHelper(userMsgQueueService, SSEPingFormatClaude, pingInterval)
  90. }
  91. return &GatewayHandler{
  92. gatewayService: gatewayService,
  93. openAIGatewayService: openAIGatewayService,
  94. geminiCompatService: geminiCompatService,
  95. antigravityGatewayService: antigravityGatewayService,
  96. userService: userService,
  97. billingCacheService: billingCacheService,
  98. usageService: usageService,
  99. apiKeyService: apiKeyService,
  100. usageRecordWorkerPool: usageRecordWorkerPool,
  101. errorPassthroughService: errorPassthroughService,
  102. contentModerationService: contentModerationService,
  103. concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatClaude, pingInterval),
  104. userMsgQueueHelper: umqHelper,
  105. maxAccountSwitches: maxAccountSwitches,
  106. maxAccountSwitchesGemini: maxAccountSwitchesGemini,
  107. cfg: cfg,
  108. settingService: settingService,
  109. }
  110. }
  111. // Messages handles Claude API compatible messages endpoint
  112. // POST /v1/messages
  113. func (h *GatewayHandler) Messages(c *gin.Context) {
  114. // 从context获取apiKey和user(ApiKeyAuth中间件已设置)
  115. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  116. if !ok {
  117. h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  118. return
  119. }
  120. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  121. if !ok {
  122. h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  123. return
  124. }
  125. reqLog := requestLogger(
  126. c,
  127. "handler.gateway.messages",
  128. zap.Int64("user_id", subject.UserID),
  129. zap.Int64("api_key_id", apiKey.ID),
  130. zap.Any("group_id", apiKey.GroupID),
  131. )
  132. defer h.maybeLogCompatibilityFallbackMetrics(reqLog)
  133. // 读取请求体
  134. body, err := readLenientJSONRequestBodyWithPrealloc(c.Request, h.cfg)
  135. if err != nil {
  136. if maxErr, ok := extractMaxBytesError(err); ok {
  137. h.errorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
  138. return
  139. }
  140. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
  141. return
  142. }
  143. if len(body) == 0 {
  144. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
  145. return
  146. }
  147. setOpsRequestContext(c, "", false)
  148. bodyRef := service.NewRequestBodyRef(body)
  149. parsedReq, err := service.ParseGatewayRequest(bodyRef, domain.PlatformAnthropic)
  150. if err != nil {
  151. logRequestBodyParseFailure(reqLog, body, err)
  152. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  153. return
  154. }
  155. body = parsedReq.Body.Bytes()
  156. reqModel := parsedReq.Model
  157. reqStream := parsedReq.Stream
  158. bindRequestedReasoningEffort(c, body, reqModel)
  159. ensureCompositeTargetPlatform(c, apiKey, reqModel)
  160. if policyBody, changed, err := applyAnthropicReasoningEffortPolicyForRequest(c, apiKey, body); err != nil {
  161. respondOpenAIReasoningEffortPolicyError(c, err, h.errorResponse)
  162. return
  163. } else if changed {
  164. if err := parsedReq.ReplaceBody(policyBody); err != nil {
  165. reqLog.Warn("gateway.reasoning_effort_policy_parse_failed", zap.Error(err))
  166. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to apply reasoning effort policy")
  167. return
  168. }
  169. body = parsedReq.Body.Bytes()
  170. reqModel = parsedReq.Model
  171. reqStream = parsedReq.Stream
  172. }
  173. reqLog = reqLog.With(zap.String("model", reqModel), zap.Bool("stream", reqStream))
  174. // 解析渠道级模型映射
  175. channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel)
  176. // 设置 max_tokens=1 + haiku 探测请求标识到 context 中
  177. // 必须在 SetClaudeCodeClientContext 之前设置,因为 ClaudeCodeValidator 需要读取此标识进行绕过判断
  178. if isMaxTokensOneHaikuRequest(reqModel, parsedReq.MaxTokens) {
  179. ctx := service.WithIsMaxTokensOneHaikuRequest(c.Request.Context(), true, h.metadataBridgeEnabled())
  180. c.Request = c.Request.WithContext(ctx)
  181. }
  182. // 检查是否为 Claude Code 客户端,设置到 context 中(复用已解析请求,避免二次反序列化)。
  183. SetClaudeCodeClientContext(c, body, parsedReq)
  184. isClaudeCodeClient := service.IsClaudeCodeClient(c.Request.Context())
  185. // 版本检查:仅对 Claude Code 客户端,拒绝低于最低版本的请求
  186. if !h.checkClaudeCodeVersion(c) {
  187. return
  188. }
  189. // 在请求上下文中记录 thinking 状态,供 Antigravity 最终模型 key 推导/模型维度限流使用
  190. c.Request = c.Request.WithContext(service.WithThinkingEnabled(c.Request.Context(), parsedReq.ThinkingEnabled, h.metadataBridgeEnabled()))
  191. setOpsRequestContext(c, reqModel, reqStream)
  192. setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(reqStream, false)))
  193. pricingCtx, pricingAt := service.WithGatewayTokenRequestPricing(c.Request.Context())
  194. c.Request = c.Request.WithContext(pricingCtx)
  195. // 验证 model 必填
  196. if reqModel == "" {
  197. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  198. return
  199. }
  200. if !compositeTargetPlatformResolved(c, apiKey, reqModel) {
  201. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by composite groups")
  202. return
  203. }
  204. if decision := h.checkSecurityAudit(c, reqLog, apiKey, subject, service.ContentModerationProtocolAnthropicMessages, reqModel, body); decision != nil && !decision.AllowNextStage {
  205. h.anthropicSecurityAuditError(c, decision)
  206. return
  207. }
  208. // Track if we've started streaming (for error handling)
  209. streamStarted := false
  210. // 绑定错误透传服务,允许 service 层在非 failover 错误场景复用规则。
  211. if h.errorPassthroughService != nil {
  212. service.BindErrorPassthroughService(c, h.errorPassthroughService)
  213. }
  214. // 获取订阅信息(可能为nil)- 提前获取用于后续检查
  215. subscription, _ := middleware2.GetSubscriptionFromContext(c)
  216. // 1. 首先获取用户并发槽位
  217. userReleaseFunc, err := h.concurrencyHelper.AcquireUserSlotWithWait(c, subject.UserID, subject.Concurrency, reqStream, &streamStarted)
  218. if err != nil {
  219. reqLog.Warn("gateway.user_slot_acquire_failed", zap.Error(err))
  220. h.handleConcurrencyError(c, err, "user", streamStarted)
  221. return
  222. }
  223. // 在请求结束或 Context 取消时确保释放槽位,避免客户端断开造成泄漏
  224. userReleaseFunc = wrapReleaseOnDone(c.Request.Context(), userReleaseFunc)
  225. if userReleaseFunc != nil {
  226. defer userReleaseFunc()
  227. }
  228. // 2. 【新增】Wait后二次检查余额/订阅
  229. if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription, service.QuotaPlatform(c.Request.Context(), apiKey)); err != nil {
  230. reqLog.Info("gateway.billing_eligibility_check_failed", zap.Error(err))
  231. status, code, message, retryAfter := billingErrorDetails(err)
  232. if retryAfter > 0 {
  233. c.Header("Retry-After", strconv.Itoa(retryAfter))
  234. }
  235. h.handleStreamingAwareError(c, status, code, message, streamStarted)
  236. return
  237. }
  238. // 设置请求所属分组 ID(用于渠道级功能判断,如 WebSearch 模拟)
  239. parsedReq.GroupID = apiKey.GroupID
  240. // 计算粘性会话hash
  241. parsedReq.SessionContext = &service.SessionContext{
  242. ClientIP: ip.GetClientIP(c),
  243. UserAgent: c.GetHeader("User-Agent"),
  244. APIKeyID: apiKey.ID,
  245. }
  246. sessionHash := h.gatewayService.GenerateSessionHash(parsedReq)
  247. // [DEBUG-STICKY] 打印会话 hash 生成结果
  248. reqLog.Info("sticky.session_hash_generated",
  249. zap.String("session_hash", sessionHash),
  250. zap.String("metadata_user_id_raw", parsedReq.MetadataUserID),
  251. )
  252. // 获取平台:优先使用强制平台(/antigravity 路由),其次使用 composite 解析出的目标平台,否则使用分组平台
  253. platform := ""
  254. if forcePlatform, ok := middleware2.GetForcePlatformFromContext(c); ok {
  255. platform = forcePlatform
  256. } else if resolvedPlatform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok {
  257. platform = resolvedPlatform
  258. } else if apiKey.Group != nil {
  259. platform = apiKey.Group.Platform
  260. }
  261. sessionKey := sessionHash
  262. if platform == service.PlatformGemini && sessionHash != "" {
  263. sessionKey = "gemini:" + sessionHash
  264. }
  265. // 查询粘性会话绑定的账号 ID
  266. var sessionBoundAccountID int64
  267. if sessionKey != "" {
  268. sessionBoundAccountID, _ = h.gatewayService.GetCachedSessionAccountID(c.Request.Context(), apiKey.GroupID, sessionKey)
  269. // [DEBUG-STICKY] 打印粘性会话查询结果
  270. reqLog.Info("sticky.cache_lookup",
  271. zap.String("session_key", sessionKey),
  272. zap.Int64("bound_account_id", sessionBoundAccountID),
  273. )
  274. if sessionBoundAccountID > 0 {
  275. prefetchedGroupID := int64(0)
  276. if apiKey.GroupID != nil {
  277. prefetchedGroupID = *apiKey.GroupID
  278. }
  279. ctx := service.WithPrefetchedStickySession(c.Request.Context(), sessionBoundAccountID, prefetchedGroupID, h.metadataBridgeEnabled())
  280. c.Request = c.Request.WithContext(ctx)
  281. }
  282. } else {
  283. reqLog.Info("sticky.no_session_key", zap.String("session_hash", sessionHash))
  284. }
  285. // 判断是否真的绑定了粘性会话:有 sessionKey 且已经绑定到某个账号
  286. hasBoundSession := sessionKey != "" && sessionBoundAccountID > 0
  287. if platform == service.PlatformGemini {
  288. fs := NewFailoverState(h.maxAccountSwitchesGemini, hasBoundSession)
  289. // 单账号分组提前设置 SingleAccountRetry 标记,让 Service 层首次 503 就不设模型限流标记。
  290. // 避免单账号分组收到 503 (MODEL_CAPACITY_EXHAUSTED) 时设 29s 限流,导致后续请求连续快速失败。
  291. if h.gatewayService.IsSingleAntigravityAccountGroup(c.Request.Context(), apiKey.GroupID) {
  292. ctx := service.WithSingleAccountRetry(c.Request.Context(), true, h.metadataBridgeEnabled())
  293. c.Request = c.Request.WithContext(ctx)
  294. }
  295. for {
  296. selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), apiKey.GroupID, sessionKey, reqModel, fs.FailedAccountIDs, "", int64(0)) // Gemini 不使用会话限制
  297. if err != nil {
  298. if len(fs.FailedAccountIDs) == 0 {
  299. cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, reqModel, reqModel, service.PlatformGemini)
  300. if !cls.ModelNotFound {
  301. markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
  302. }
  303. reqLog.Warn("gateway.select_account_no_available",
  304. zap.String("model", reqModel),
  305. zap.Int64p("group_id", apiKey.GroupID),
  306. zap.String("platform", platform),
  307. zap.Bool("model_not_found", cls.ModelNotFound),
  308. zap.Error(err),
  309. )
  310. message := cls.Message
  311. if !cls.ModelNotFound {
  312. message = "No available accounts: " + err.Error()
  313. }
  314. h.handleStreamingAwareError(c, cls.Status, cls.ErrType, message, streamStarted)
  315. return
  316. }
  317. action := fs.HandleSelectionExhausted(c.Request.Context())
  318. switch action {
  319. case FailoverContinue:
  320. ctx := service.WithSingleAccountRetry(c.Request.Context(), true, h.metadataBridgeEnabled())
  321. c.Request = c.Request.WithContext(ctx)
  322. continue
  323. case FailoverCanceled:
  324. failoverClientGone(c)
  325. return
  326. default: // FailoverExhausted
  327. if fs.LastFailoverErr != nil {
  328. h.handleFailoverExhausted(c, fs.LastFailoverErr, service.PlatformGemini, streamStarted)
  329. } else {
  330. h.handleFailoverExhaustedSimple(c, 502, streamStarted)
  331. }
  332. return
  333. }
  334. }
  335. account := selection.Account
  336. setOpsSelectedAccount(c, account.ID, account.Platform)
  337. // 检查请求拦截(预热请求、SUGGESTION MODE等)
  338. if account.IsInterceptWarmupEnabled() {
  339. interceptType := detectInterceptType(body, reqModel, parsedReq.MaxTokens, isClaudeCodeClient)
  340. if interceptType != InterceptTypeNone {
  341. if selection.Acquired && selection.ReleaseFunc != nil {
  342. selection.ReleaseFunc()
  343. }
  344. if reqStream {
  345. sendMockInterceptStream(c, reqModel, interceptType)
  346. } else {
  347. sendMockInterceptResponse(c, reqModel, interceptType)
  348. }
  349. return
  350. }
  351. }
  352. // 3. 获取账号并发槽位
  353. accountReleaseFunc := selection.ReleaseFunc
  354. if !selection.Acquired {
  355. if selection.WaitPlan == nil {
  356. markOpsRoutingCapacityLimited(c)
  357. reqLog.Warn("gateway.select_account_no_slot_no_wait_plan",
  358. zap.Int64("account_id", account.ID),
  359. zap.String("model", reqModel),
  360. zap.String("platform", platform),
  361. )
  362. h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts", streamStarted)
  363. return
  364. }
  365. accountWaitCounted := false
  366. canWait, err := h.concurrencyHelper.IncrementAccountWaitCount(c.Request.Context(), account.ID, selection.WaitPlan.MaxWaiting)
  367. if err != nil {
  368. reqLog.Warn("gateway.account_wait_counter_increment_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  369. } else if !canWait {
  370. reqLog.Info("gateway.account_wait_queue_full",
  371. zap.Int64("account_id", account.ID),
  372. zap.Int("max_waiting", selection.WaitPlan.MaxWaiting),
  373. )
  374. h.handleStreamingAwareErrorWithCode(c, http.StatusTooManyRequests, "rate_limit_error", gatewayQueueFullCode, "Too many pending requests, please retry later", streamStarted)
  375. return
  376. }
  377. if err == nil && canWait {
  378. accountWaitCounted = true
  379. }
  380. releaseWait := func() {
  381. if accountWaitCounted {
  382. h.concurrencyHelper.DecrementAccountWaitCount(c.Request.Context(), account.ID)
  383. accountWaitCounted = false
  384. }
  385. }
  386. accountReleaseFunc, err = h.concurrencyHelper.AcquireAccountSlotWithWaitTimeout(
  387. c,
  388. account.ID,
  389. selection.WaitPlan.MaxConcurrency,
  390. selection.WaitPlan.Timeout,
  391. reqStream,
  392. &streamStarted,
  393. )
  394. if err != nil {
  395. reqLog.Warn("gateway.account_slot_acquire_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  396. releaseWait()
  397. h.handleConcurrencyError(c, err, "account", streamStarted)
  398. return
  399. }
  400. // Slot acquired: no longer waiting in queue.
  401. releaseWait()
  402. }
  403. // 终检与准入后绑定使用选号结果携带的门(见 responses 同名注释)。
  404. admissionCtx := service.ContextWithSelectionProfitGate(c.Request.Context(), selection)
  405. latest, vetoed, reason := h.gatewayService.GatewayProfitControlVetoLatest(admissionCtx, account)
  406. if vetoed {
  407. if accountReleaseFunc != nil {
  408. accountReleaseFunc()
  409. }
  410. reqLog.Debug("gateway.account_slot_profit_vetoed", zap.Int64("account_id", account.ID), zap.String("reason", reason))
  411. if fs.RecordProfitVeto(account.ID) == FailoverExhausted {
  412. reqLog.Warn("gateway.profit_veto_attempts_exhausted", zap.Int("profit_veto_count", fs.ProfitVetoCount()))
  413. markOpsRoutingCapacityLimited(c)
  414. h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", profitVetoExhaustedMessage, streamStarted)
  415. return
  416. }
  417. continue
  418. }
  419. account = latest
  420. selection.Account = latest
  421. // 等待路径保持既有 eager 绑定(无门时 helper 直接绑定);调度器已
  422. // 抢槽的直达路径无门时由选号内部绑定,这里只在门下补准入后绑定。
  423. if selection.ProfitGateActive() || !selection.Acquired {
  424. if err := h.gatewayService.BindStickySessionAfterProfitAdmission(admissionCtx, apiKey.GroupID, sessionKey, account.ID); err != nil {
  425. reqLog.Warn("gateway.bind_sticky_session_after_profit_admission_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  426. }
  427. }
  428. // 账号槽位/等待计数需要在超时或断开时安全回收
  429. accountReleaseFunc = wrapReleaseOnDone(c.Request.Context(), accountReleaseFunc)
  430. // 转发请求 - 根据账号平台分流
  431. var result *service.ForwardResult
  432. requestCtx := c.Request.Context()
  433. if fs.SwitchCount > 0 {
  434. requestCtx = service.WithAccountSwitchCount(requestCtx, fs.SwitchCount, h.metadataBridgeEnabled())
  435. }
  436. // 记录 Forward 前已写入字节数,Forward 后若增加则说明 SSE 内容已发,禁止 failover
  437. writerSizeBeforeForward := c.Writer.Size()
  438. if account.Platform == service.PlatformAntigravity {
  439. result, err = h.antigravityGatewayService.ForwardGemini(
  440. requestCtx,
  441. c,
  442. account,
  443. reqModel,
  444. "generateContent",
  445. reqStream,
  446. body,
  447. hasBoundSession,
  448. service.WithForwardGeminiSession(derefGroupID(apiKey.GroupID), sessionKey),
  449. )
  450. } else {
  451. result, err = h.geminiCompatService.Forward(requestCtx, c, account, body)
  452. }
  453. if accountReleaseFunc != nil {
  454. accountReleaseFunc()
  455. }
  456. if err != nil {
  457. var failoverErr *service.UpstreamFailoverError
  458. if errors.As(err, &failoverErr) {
  459. // 流式内容已写入客户端,无法撤销,禁止 failover 以防止流拼接腐化
  460. if c.Writer.Size() != writerSizeBeforeForward {
  461. h.handleFailoverExhausted(c, failoverErr, service.PlatformGemini, true)
  462. return
  463. }
  464. action := fs.HandleFailoverError(c.Request.Context(), h.gatewayService, account.ID, account.Platform, account.GetPoolModeRetryCount(), failoverErr)
  465. switch action {
  466. case FailoverContinue:
  467. continue
  468. case FailoverExhausted:
  469. h.handleFailoverExhausted(c, fs.LastFailoverErr, service.PlatformGemini, streamStarted)
  470. return
  471. case FailoverCanceled:
  472. failoverClientGone(c)
  473. return
  474. }
  475. }
  476. upstreamErrorAlreadyCommunicated := gatewayForwardErrorAlreadyCommunicated(c, writerSizeBeforeForward, err)
  477. wroteFallback := false
  478. if !upstreamErrorAlreadyCommunicated {
  479. wroteFallback = h.ensureForwardErrorResponse(c, streamStarted)
  480. }
  481. forwardFailedFields := []zap.Field{
  482. zap.Int64("account_id", account.ID),
  483. zap.String("account_name", account.Name),
  484. zap.String("account_platform", account.Platform),
  485. zap.Bool("fallback_error_response_written", wroteFallback),
  486. zap.Bool("upstream_error_response_already_written", upstreamErrorAlreadyCommunicated),
  487. zap.Error(err),
  488. }
  489. if account.Proxy != nil {
  490. forwardFailedFields = append(forwardFailedFields,
  491. zap.Int64("proxy_id", account.Proxy.ID),
  492. zap.String("proxy_name", account.Proxy.Name),
  493. zap.String("proxy_host", account.Proxy.Host),
  494. zap.Int("proxy_port", account.Proxy.Port),
  495. )
  496. } else if account.ProxyID != nil {
  497. forwardFailedFields = append(forwardFailedFields, zap.Int64p("proxy_id", account.ProxyID))
  498. }
  499. reqLog.Error("gateway.forward_failed", forwardFailedFields...)
  500. return
  501. }
  502. // RPM 计数递增(Forward 成功后)
  503. // 注意:TOCTOU 竞态是已知且可接受的设计权衡,与 WindowCost 一致的 soft-limit 模式。
  504. // 在高并发下可能短暂超出 RPM 限制,但不会导致请求失败。
  505. if account.IsAnthropicOAuthOrSetupToken() && account.GetBaseRPM() > 0 {
  506. if err := h.gatewayService.IncrementAccountRPM(c.Request.Context(), account.ID); err != nil {
  507. reqLog.Warn("gateway.rpm_increment_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  508. }
  509. }
  510. // 捕获请求信息(用于异步记录,避免在 goroutine 中访问 gin.Context)
  511. userAgent := c.GetHeader("User-Agent")
  512. clientIP := ip.GetClientIP(c)
  513. requestPayloadHash := service.HashUsageRequestPayload(body)
  514. inboundEndpoint := GetInboundEndpoint(c)
  515. upstreamEndpoint := GetUpstreamEndpoint(c, account.Platform)
  516. stampForwardRequestedReasoningEffort(result, service.NormalizeClaudeOutputEffort(parsedReq.OutputEffort))
  517. if result.ReasoningEffort == nil {
  518. result.ReasoningEffort = service.NormalizeClaudeOutputEffort(parsedReq.OutputEffort)
  519. }
  520. // 国产模型 thinking-enabled 默认 effort 填充:Kimi/GLM/MiniMax 这些不支持 effort 档位的
  521. // passback-required 上游,仅要 thinking 启用且 OutputEffort 未明确传递时,在 usage_log 写 "high"
  522. // 避免该字段长期为 NULL(详见 DefaultEffortForThinkingEnabled 文档)。
  523. if result.ReasoningEffort == nil && parsedReq.ThinkingEnabled {
  524. protocolModel := result.UpstreamModel
  525. if protocolModel == "" {
  526. protocolModel = result.Model
  527. }
  528. result.ReasoningEffort = service.DefaultEffortForThinkingEnabled(protocolModel)
  529. }
  530. // 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
  531. // ForceCacheBilling 提前拍成标量,避免 worker 闭包保活 failover 状态里的响应体。
  532. forceCacheBilling := fs.ForceCacheBilling
  533. quotaPlatform := service.QuotaPlatform(c.Request.Context(), apiKey)
  534. sessionID := service.ExtractClientSessionID(c)
  535. h.submitUsageRecordTask(c.Request.Context(), func(ctx context.Context) {
  536. if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
  537. Result: result,
  538. QuotaPlatform: quotaPlatform,
  539. APIKey: apiKey,
  540. User: apiKey.User,
  541. Account: account,
  542. Subscription: subscription,
  543. PricingAt: pricingAt,
  544. InboundEndpoint: inboundEndpoint,
  545. UpstreamEndpoint: upstreamEndpoint,
  546. UserAgent: userAgent,
  547. IPAddress: clientIP,
  548. SessionID: sessionID,
  549. RequestPayloadHash: requestPayloadHash,
  550. ForceCacheBilling: forceCacheBilling,
  551. APIKeyService: h.apiKeyService,
  552. ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
  553. }); err != nil {
  554. logger.L().With(
  555. zap.String("component", "handler.gateway.messages"),
  556. zap.Int64("user_id", subject.UserID),
  557. zap.Int64("api_key_id", apiKey.ID),
  558. zap.Any("group_id", apiKey.GroupID),
  559. zap.String("model", reqModel),
  560. zap.Int64("account_id", account.ID),
  561. ).Error("gateway.record_usage_failed", zap.Error(err))
  562. }
  563. })
  564. return
  565. }
  566. }
  567. currentAPIKey := apiKey
  568. currentSubscription := subscription
  569. var fallbackGroupID *int64
  570. if apiKey.Group != nil {
  571. fallbackGroupID = apiKey.Group.FallbackGroupIDOnInvalidRequest
  572. }
  573. fallbackUsed := false
  574. // 单账号分组提前设置 SingleAccountRetry 标记,让 Service 层首次 503 就不设模型限流标记。
  575. // 避免单账号分组收到 503 (MODEL_CAPACITY_EXHAUSTED) 时设 29s 限流,导致后续请求连续快速失败。
  576. if h.gatewayService.IsSingleAntigravityAccountGroup(c.Request.Context(), currentAPIKey.GroupID) {
  577. ctx := service.WithSingleAccountRetry(c.Request.Context(), true, h.metadataBridgeEnabled())
  578. c.Request = c.Request.WithContext(ctx)
  579. }
  580. // 会话槽失败释放:failover 链上每个选中的 Anthropic OAuth 账号都注册过同一会话
  581. // (checkAndRegisterSession)。若请求最终失败(转发失败/客户端中断/换号耗尽),
  582. // 须立即释放本次会话注册——上游从未真正服务该会话,继续占槽会让 max_sessions
  583. // 受限的账号被失败请求的 session hash 卡满整个空闲窗口,后续新会话全部被拒。
  584. // 成功请求保持既有空闲超时语义(会话按最后活动时间过期)。
  585. sessionSlotAccounts := make(map[int64]*service.Account)
  586. upstreamServedSession := false
  587. defer func() {
  588. if upstreamServedSession {
  589. return
  590. }
  591. // 客户端可能已断开、请求 ctx 已取消,用独立 ctx 执行释放
  592. for _, acc := range sessionSlotAccounts {
  593. h.gatewayService.ReleaseAccountSession(context.Background(), acc, sessionKey)
  594. }
  595. }()
  596. for {
  597. fs := NewFailoverState(h.maxAccountSwitches, hasBoundSession)
  598. retryWithFallback := false
  599. for {
  600. attemptParsedReq, err := parsedReq.CloneForBody(body)
  601. if err != nil {
  602. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  603. return
  604. }
  605. // 选择支持该模型的账号
  606. reqLog.Info("sticky.selecting_account",
  607. zap.String("session_key", sessionKey),
  608. zap.Int64("sticky_bound_account_id", sessionBoundAccountID),
  609. zap.Bool("has_bound_session", hasBoundSession),
  610. zap.Int("failed_account_count", len(fs.FailedAccountIDs)),
  611. )
  612. selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), currentAPIKey.GroupID, sessionKey, reqModel, fs.FailedAccountIDs, parsedReq.MetadataUserID, subject.UserID)
  613. if err != nil {
  614. if len(fs.FailedAccountIDs) == 0 {
  615. cls := classifyNoAccountErrorFromGin(c, h.gatewayService, currentAPIKey, reqModel, reqModel, platform)
  616. if !cls.ModelNotFound {
  617. markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
  618. }
  619. reqLog.Warn("gateway.select_account_no_available",
  620. zap.String("model", reqModel),
  621. zap.Int64p("group_id", currentAPIKey.GroupID),
  622. zap.String("platform", platform),
  623. zap.Bool("fallback_used", fallbackUsed),
  624. zap.Bool("model_not_found", cls.ModelNotFound),
  625. zap.Error(err),
  626. )
  627. message := cls.Message
  628. if !cls.ModelNotFound {
  629. message = "No available accounts: " + err.Error()
  630. }
  631. h.handleStreamingAwareError(c, cls.Status, cls.ErrType, message, streamStarted)
  632. return
  633. }
  634. action := fs.HandleSelectionExhausted(c.Request.Context())
  635. switch action {
  636. case FailoverContinue:
  637. ctx := service.WithSingleAccountRetry(c.Request.Context(), true, h.metadataBridgeEnabled())
  638. c.Request = c.Request.WithContext(ctx)
  639. continue
  640. case FailoverCanceled:
  641. failoverClientGone(c)
  642. return
  643. default: // FailoverExhausted
  644. if fs.LastFailoverErr != nil {
  645. h.handleFailoverExhausted(c, fs.LastFailoverErr, platform, streamStarted)
  646. } else {
  647. h.handleFailoverExhaustedSimple(c, 502, streamStarted)
  648. }
  649. return
  650. }
  651. }
  652. account := selection.Account
  653. setOpsSelectedAccount(c, account.ID, account.Platform)
  654. // [DEBUG-STICKY] 打印账号选择结果
  655. reqLog.Info("sticky.account_selected",
  656. zap.Int64("selected_account_id", account.ID),
  657. zap.String("account_name", account.Name),
  658. zap.Bool("slot_acquired", selection.Acquired),
  659. zap.Bool("has_wait_plan", selection.WaitPlan != nil),
  660. zap.Int64("sticky_bound_account_id", sessionBoundAccountID),
  661. zap.Bool("sticky_honored", sessionBoundAccountID > 0 && sessionBoundAccountID == account.ID),
  662. )
  663. // 检查请求拦截(预热请求、SUGGESTION MODE等)
  664. if account.IsInterceptWarmupEnabled() {
  665. interceptType := detectInterceptType(body, reqModel, parsedReq.MaxTokens, isClaudeCodeClient)
  666. if interceptType != InterceptTypeNone {
  667. if selection.Acquired && selection.ReleaseFunc != nil {
  668. selection.ReleaseFunc()
  669. }
  670. if reqStream {
  671. sendMockInterceptStream(c, reqModel, interceptType)
  672. } else {
  673. sendMockInterceptResponse(c, reqModel, interceptType)
  674. }
  675. return
  676. }
  677. }
  678. // 3. 获取账号并发槽位
  679. accountReleaseFunc := selection.ReleaseFunc
  680. if !selection.Acquired {
  681. if selection.WaitPlan == nil {
  682. markOpsRoutingCapacityLimited(c)
  683. reqLog.Warn("gateway.select_account_no_slot_no_wait_plan",
  684. zap.Int64("account_id", account.ID),
  685. zap.String("model", reqModel),
  686. zap.String("platform", platform),
  687. )
  688. h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts", streamStarted)
  689. return
  690. }
  691. accountWaitCounted := false
  692. canWait, err := h.concurrencyHelper.IncrementAccountWaitCount(c.Request.Context(), account.ID, selection.WaitPlan.MaxWaiting)
  693. if err != nil {
  694. reqLog.Warn("gateway.account_wait_counter_increment_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  695. } else if !canWait {
  696. reqLog.Info("gateway.account_wait_queue_full",
  697. zap.Int64("account_id", account.ID),
  698. zap.Int("max_waiting", selection.WaitPlan.MaxWaiting),
  699. )
  700. h.handleStreamingAwareErrorWithCode(c, http.StatusTooManyRequests, "rate_limit_error", gatewayQueueFullCode, "Too many pending requests, please retry later", streamStarted)
  701. return
  702. }
  703. if err == nil && canWait {
  704. accountWaitCounted = true
  705. }
  706. releaseWait := func() {
  707. if accountWaitCounted {
  708. h.concurrencyHelper.DecrementAccountWaitCount(c.Request.Context(), account.ID)
  709. accountWaitCounted = false
  710. }
  711. }
  712. accountReleaseFunc, err = h.concurrencyHelper.AcquireAccountSlotWithWaitTimeout(
  713. c,
  714. account.ID,
  715. selection.WaitPlan.MaxConcurrency,
  716. selection.WaitPlan.Timeout,
  717. reqStream,
  718. &streamStarted,
  719. )
  720. if err != nil {
  721. reqLog.Warn("gateway.account_slot_acquire_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  722. releaseWait()
  723. h.handleConcurrencyError(c, err, "account", streamStarted)
  724. return
  725. }
  726. // Slot acquired: no longer waiting in queue.
  727. releaseWait()
  728. }
  729. // 终检与准入后绑定使用选号结果携带的门(见 responses 同名注释)。
  730. admissionCtx := service.ContextWithSelectionProfitGate(c.Request.Context(), selection)
  731. latest, vetoed, reason := h.gatewayService.GatewayProfitControlVetoLatest(admissionCtx, account)
  732. if vetoed {
  733. if accountReleaseFunc != nil {
  734. accountReleaseFunc()
  735. }
  736. reqLog.Debug("gateway.account_slot_profit_vetoed", zap.Int64("account_id", account.ID), zap.String("reason", reason))
  737. if fs.RecordProfitVeto(account.ID) == FailoverExhausted {
  738. reqLog.Warn("gateway.profit_veto_attempts_exhausted", zap.Int("profit_veto_count", fs.ProfitVetoCount()))
  739. markOpsRoutingCapacityLimited(c)
  740. h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", profitVetoExhaustedMessage, streamStarted)
  741. return
  742. }
  743. // 尝试被否决(从未转发),立即释放该账号的会话注册
  744. h.gatewayService.ReleaseAccountSession(context.Background(), account, sessionKey)
  745. delete(sessionSlotAccounts, account.ID)
  746. continue
  747. }
  748. account = latest
  749. selection.Account = latest
  750. // 记录本请求注册过会话槽的账号(profit 准入后账号已定)
  751. sessionSlotAccounts[account.ID] = account
  752. // 等待路径保持既有 eager 绑定(无门时 helper 直接绑定);调度器已
  753. // 抢槽的直达路径无门时由选号内部绑定,这里只在门下补准入后绑定。
  754. if selection.ProfitGateActive() || !selection.Acquired {
  755. if err := h.gatewayService.BindStickySessionAfterProfitAdmission(admissionCtx, currentAPIKey.GroupID, sessionKey, account.ID); err != nil {
  756. reqLog.Warn("gateway.bind_sticky_session_after_profit_admission_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  757. }
  758. }
  759. // 账号槽位/等待计数需要在超时或断开时安全回收
  760. accountReleaseFunc = wrapReleaseOnDone(c.Request.Context(), accountReleaseFunc)
  761. // ===== 用户消息串行队列 START =====
  762. var queueRelease func()
  763. umqMode := h.getUserMsgQueueMode(account, attemptParsedReq)
  764. switch umqMode {
  765. case config.UMQModeSerialize:
  766. // 串行模式:获取锁 + RPM 延迟 + 释放(当前行为不变)
  767. baseRPM := account.GetBaseRPM()
  768. release, qErr := h.userMsgQueueHelper.AcquireWithWait(
  769. c, account.ID, baseRPM, reqStream, &streamStarted,
  770. h.cfg.Gateway.UserMessageQueue.WaitTimeout(),
  771. reqLog,
  772. )
  773. if qErr != nil {
  774. // fail-open: 记录 warn,不阻止请求
  775. reqLog.Warn("gateway.umq_acquire_failed",
  776. zap.Int64("account_id", account.ID),
  777. zap.Error(qErr),
  778. )
  779. } else {
  780. queueRelease = release
  781. }
  782. case config.UMQModeThrottle:
  783. // 软性限速:仅施加 RPM 自适应延迟,不阻塞并发
  784. baseRPM := account.GetBaseRPM()
  785. if tErr := h.userMsgQueueHelper.ThrottleWithPing(
  786. c, account.ID, baseRPM, reqStream, &streamStarted,
  787. h.cfg.Gateway.UserMessageQueue.WaitTimeout(),
  788. reqLog,
  789. ); tErr != nil {
  790. reqLog.Warn("gateway.umq_throttle_failed",
  791. zap.Int64("account_id", account.ID),
  792. zap.Error(tErr),
  793. )
  794. }
  795. default:
  796. if umqMode != "" {
  797. reqLog.Warn("gateway.umq_unknown_mode",
  798. zap.String("mode", umqMode),
  799. zap.Int64("account_id", account.ID),
  800. )
  801. }
  802. }
  803. // 用 wrapReleaseOnDone 确保 context 取消时自动释放(仅 serialize 模式有 queueRelease)
  804. queueRelease = wrapReleaseOnDone(c.Request.Context(), queueRelease)
  805. // 注入回调到 ParsedRequest:使用外层 wrapper 以便提前清理 AfterFunc
  806. attemptParsedReq.OnUpstreamAccepted = queueRelease
  807. // ===== 用户消息串行队列 END =====
  808. // 渠道模型映射只作用于本次账号尝试,避免 failover 后污染原始 ParsedRequest。
  809. if channelMapping.Mapped {
  810. attemptParsedReq.Model = channelMapping.MappedModel
  811. if err := attemptParsedReq.ReplaceBody(h.gatewayService.ReplaceModelInBody(attemptParsedReq.Body.Bytes(), channelMapping.MappedModel)); err != nil {
  812. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  813. return
  814. }
  815. }
  816. // Bedrock CC 兼容:清理 body 专有字段 + 过滤 anthropic-beta header,适用于所有转发路径
  817. if err := attemptParsedReq.ReplaceBody(h.gatewayService.ApplyBedrockCCCompat(c, attemptParsedReq.Body.Bytes(), attemptParsedReq.Model, account, apiKey.GroupID)); err != nil {
  818. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  819. return
  820. }
  821. attemptBody := attemptParsedReq.Body.Bytes()
  822. // 转发请求 - 根据账号平台分流
  823. c.Set("parsed_request", attemptParsedReq)
  824. var result *service.ForwardResult
  825. requestCtx := c.Request.Context()
  826. if fs.SwitchCount > 0 {
  827. requestCtx = service.WithAccountSwitchCount(requestCtx, fs.SwitchCount, h.metadataBridgeEnabled())
  828. }
  829. if fs.ForceCacheBilling {
  830. requestCtx = service.WithForceCacheBilling(requestCtx)
  831. }
  832. // 记录 Forward 前已写入字节数,Forward 后若增加则说明 SSE 内容已发,禁止 failover
  833. writerSizeBeforeForward := c.Writer.Size()
  834. if account.Platform == service.PlatformAntigravity && account.Type != service.AccountTypeAPIKey {
  835. result, err = h.antigravityGatewayService.Forward(requestCtx, c, account, attemptBody, hasBoundSession)
  836. } else {
  837. result, err = h.gatewayService.Forward(requestCtx, c, account, attemptParsedReq)
  838. }
  839. // 兜底释放串行锁(正常情况已通过回调提前释放)
  840. if queueRelease != nil {
  841. queueRelease()
  842. }
  843. // 清理回调引用,防止 failover 重试时旧回调被错误调用
  844. attemptParsedReq.OnUpstreamAccepted = nil
  845. if accountReleaseFunc != nil {
  846. accountReleaseFunc()
  847. }
  848. // 提交 usage 记录。成功路径与"流中断但 Forward 已观测到 usage 的部分结果"
  849. // 错误路径共用:后者若不入账,上游已计量的请求会完全漏记漏计费(#5148)。
  850. submitForwardUsage := func(result *service.ForwardResult) {
  851. // 捕获请求信息(用于异步记录,避免在 goroutine 中访问 gin.Context)
  852. userAgent := c.GetHeader("User-Agent")
  853. clientIP := ip.GetClientIP(c)
  854. // Forward 内部可能继续改写 body,usage 去重指纹必须使用最终上游接受的当前 body。
  855. requestPayloadHash := service.HashUsageRequestPayload(attemptParsedReq.Body.Bytes())
  856. inboundEndpoint := GetInboundEndpoint(c)
  857. upstreamEndpoint := GetUpstreamEndpoint(c, account.Platform)
  858. stampForwardRequestedReasoningEffort(result, service.NormalizeClaudeOutputEffort(attemptParsedReq.OutputEffort))
  859. if result.ReasoningEffort == nil {
  860. result.ReasoningEffort = service.NormalizeClaudeOutputEffort(attemptParsedReq.OutputEffort)
  861. }
  862. // 同上(重试路径中的对称填充)。详见非重试路径同名注释。
  863. if result.ReasoningEffort == nil && attemptParsedReq.ThinkingEnabled {
  864. protocolModel := result.UpstreamModel
  865. if protocolModel == "" {
  866. protocolModel = result.Model
  867. }
  868. result.ReasoningEffort = service.DefaultEffortForThinkingEnabled(protocolModel)
  869. }
  870. // 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
  871. // ForceCacheBilling 提前拍成标量,避免 worker 闭包保活 failover 状态里的响应体。
  872. forceCacheBilling := fs.ForceCacheBilling
  873. quotaPlatform := service.QuotaPlatform(c.Request.Context(), currentAPIKey)
  874. sessionID := service.ExtractClientSessionID(c)
  875. h.submitUsageRecordTask(c.Request.Context(), func(ctx context.Context) {
  876. if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
  877. Result: result,
  878. QuotaPlatform: quotaPlatform,
  879. APIKey: currentAPIKey,
  880. User: currentAPIKey.User,
  881. Account: account,
  882. Subscription: currentSubscription,
  883. PricingAt: pricingAt,
  884. InboundEndpoint: inboundEndpoint,
  885. UpstreamEndpoint: upstreamEndpoint,
  886. UserAgent: userAgent,
  887. IPAddress: clientIP,
  888. SessionID: sessionID,
  889. RequestPayloadHash: requestPayloadHash,
  890. ForceCacheBilling: forceCacheBilling,
  891. APIKeyService: h.apiKeyService,
  892. ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, result.UpstreamModel),
  893. }); err != nil {
  894. logger.L().With(
  895. zap.String("component", "handler.gateway.messages"),
  896. zap.Int64("user_id", subject.UserID),
  897. zap.Int64("api_key_id", currentAPIKey.ID),
  898. zap.Any("group_id", currentAPIKey.GroupID),
  899. zap.String("model", reqModel),
  900. zap.Int64("account_id", account.ID),
  901. ).Error("gateway.record_usage_failed", zap.Error(err))
  902. }
  903. })
  904. }
  905. if err != nil {
  906. // Beta policy block: return 400 immediately, no failover
  907. var betaBlockedErr *service.BetaBlockedError
  908. if errors.As(err, &betaBlockedErr) {
  909. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalPolicyDenied)
  910. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", betaBlockedErr.Message)
  911. return
  912. }
  913. var promptTooLongErr *service.PromptTooLongError
  914. if errors.As(err, &promptTooLongErr) {
  915. reqLog.Warn("gateway.prompt_too_long_from_antigravity",
  916. zap.Any("current_group_id", currentAPIKey.GroupID),
  917. zap.Any("fallback_group_id", fallbackGroupID),
  918. zap.Bool("fallback_used", fallbackUsed),
  919. )
  920. if !fallbackUsed && fallbackGroupID != nil && *fallbackGroupID > 0 {
  921. fallbackGroup, err := h.gatewayService.ResolveGroupByID(c.Request.Context(), *fallbackGroupID)
  922. if err != nil {
  923. reqLog.Warn("gateway.resolve_fallback_group_failed", zap.Int64("fallback_group_id", *fallbackGroupID), zap.Error(err))
  924. _ = h.antigravityGatewayService.WriteMappedClaudeError(c, account, promptTooLongErr.StatusCode, promptTooLongErr.RequestID, promptTooLongErr.Body)
  925. return
  926. }
  927. if fallbackGroup.Platform != service.PlatformAnthropic ||
  928. fallbackGroup.SubscriptionType == service.SubscriptionTypeSubscription ||
  929. fallbackGroup.FallbackGroupIDOnInvalidRequest != nil {
  930. reqLog.Warn("gateway.fallback_group_invalid",
  931. zap.Int64("fallback_group_id", fallbackGroup.ID),
  932. zap.String("fallback_platform", fallbackGroup.Platform),
  933. zap.String("fallback_subscription_type", fallbackGroup.SubscriptionType),
  934. )
  935. _ = h.antigravityGatewayService.WriteMappedClaudeError(c, account, promptTooLongErr.StatusCode, promptTooLongErr.RequestID, promptTooLongErr.Body)
  936. return
  937. }
  938. fallbackAPIKey := cloneAPIKeyWithGroup(apiKey, fallbackGroup)
  939. if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), fallbackAPIKey.User, fallbackAPIKey, fallbackGroup, nil, service.PlatformFromAPIKey(fallbackAPIKey)); err != nil {
  940. status, code, message, retryAfter := billingErrorDetails(err)
  941. if retryAfter > 0 {
  942. c.Header("Retry-After", strconv.Itoa(retryAfter))
  943. }
  944. h.handleStreamingAwareError(c, status, code, message, streamStarted)
  945. return
  946. }
  947. // 兜底重试按"直接请求兜底分组"处理:清除强制平台,允许按分组平台调度
  948. ctx := context.WithValue(c.Request.Context(), ctxkey.ForcePlatform, "")
  949. c.Request = c.Request.WithContext(ctx)
  950. currentAPIKey = fallbackAPIKey
  951. currentSubscription = nil
  952. fallbackUsed = true
  953. retryWithFallback = true
  954. // 原分组账号已确定性失败(prompt too long),先释放其会话注册再走兜底分组
  955. for _, acc := range sessionSlotAccounts {
  956. h.gatewayService.ReleaseAccountSession(context.Background(), acc, sessionKey)
  957. }
  958. sessionSlotAccounts = make(map[int64]*service.Account)
  959. break
  960. }
  961. _ = h.antigravityGatewayService.WriteMappedClaudeError(c, account, promptTooLongErr.StatusCode, promptTooLongErr.RequestID, promptTooLongErr.Body)
  962. return
  963. }
  964. var failoverErr *service.UpstreamFailoverError
  965. if errors.As(err, &failoverErr) {
  966. // 流式内容已写入客户端,无法撤销,禁止 failover 以防止流拼接腐化
  967. if c.Writer.Size() != writerSizeBeforeForward {
  968. h.handleFailoverExhausted(c, failoverErr, account.Platform, true)
  969. return
  970. }
  971. action := fs.HandleFailoverError(c.Request.Context(), h.gatewayService, account.ID, account.Platform, account.GetPoolModeRetryCount(), failoverErr)
  972. switch action {
  973. case FailoverContinue:
  974. // 本次尝试已确定性失败,立即释放该账号的会话注册
  975. h.gatewayService.ReleaseAccountSession(context.Background(), account, sessionKey)
  976. delete(sessionSlotAccounts, account.ID)
  977. continue
  978. case FailoverExhausted:
  979. h.handleFailoverExhausted(c, fs.LastFailoverErr, account.Platform, streamStarted)
  980. return
  981. case FailoverCanceled:
  982. failoverClientGone(c)
  983. return
  984. }
  985. }
  986. upstreamErrorAlreadyCommunicated := gatewayForwardErrorAlreadyCommunicated(c, writerSizeBeforeForward, err)
  987. wroteFallback := false
  988. if !upstreamErrorAlreadyCommunicated {
  989. wroteFallback = h.ensureForwardErrorResponse(c, streamStarted)
  990. }
  991. forwardFailedFields := []zap.Field{
  992. zap.Int64("account_id", account.ID),
  993. zap.String("account_name", account.Name),
  994. zap.String("account_platform", account.Platform),
  995. zap.Bool("fallback_error_response_written", wroteFallback),
  996. zap.Bool("upstream_error_response_already_written", upstreamErrorAlreadyCommunicated),
  997. zap.Error(err),
  998. }
  999. if account.Proxy != nil {
  1000. forwardFailedFields = append(forwardFailedFields,
  1001. zap.Int64("proxy_id", account.Proxy.ID),
  1002. zap.String("proxy_name", account.Proxy.Name),
  1003. zap.String("proxy_host", account.Proxy.Host),
  1004. zap.Int("proxy_port", account.Proxy.Port),
  1005. )
  1006. } else if account.ProxyID != nil {
  1007. forwardFailedFields = append(forwardFailedFields, zap.Int64p("proxy_id", account.ProxyID))
  1008. }
  1009. reqLog.Error("gateway.forward_failed", forwardFailedFields...)
  1010. // Forward 与错误一起返回的部分结果:流中断前上游已计量的 usage 照常入账,
  1011. // 避免上游已产生消耗的请求完全漏记(#5148)。failover 错误恒定 result=nil,
  1012. // 不会走到这里重复计费。
  1013. if result != nil {
  1014. submitForwardUsage(result)
  1015. // 上游已接受并计量本次会话(流中断),会话槽保持既有语义
  1016. upstreamServedSession = true
  1017. }
  1018. return
  1019. }
  1020. // RPM 计数递增(Forward 成功后)
  1021. // 注意:TOCTOU 竞态是已知且可接受的设计权衡,与 WindowCost 一致的 soft-limit 模式。
  1022. // 在高并发下可能短暂超出 RPM 限制,但不会导致请求失败。
  1023. if account.IsAnthropicOAuthOrSetupToken() && account.GetBaseRPM() > 0 {
  1024. if err := h.gatewayService.IncrementAccountRPM(c.Request.Context(), account.ID); err != nil {
  1025. reqLog.Warn("gateway.rpm_increment_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  1026. }
  1027. }
  1028. // 绑定粘性会话(成功转发后绑定/刷新)
  1029. // - 无现有绑定(首次请求):创建绑定
  1030. // - 选中账号与粘性账号一致:刷新 TTL
  1031. // - 粘性账号因负载/RPM 被跳过、选中了其他账号:不覆盖原绑定,
  1032. // 下次请求粘性账号恢复后仍可命中
  1033. if sessionKey != "" && (sessionBoundAccountID == 0 || sessionBoundAccountID == account.ID) {
  1034. if err := h.gatewayService.BindStickySession(c.Request.Context(), currentAPIKey.GroupID, sessionKey, account.ID); err != nil {
  1035. reqLog.Warn("gateway.bind_sticky_session_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  1036. }
  1037. }
  1038. submitForwardUsage(result)
  1039. // 转发成功,会话槽保持既有空闲超时语义
  1040. upstreamServedSession = true
  1041. return
  1042. }
  1043. if !retryWithFallback {
  1044. return
  1045. }
  1046. }
  1047. }
  1048. // Models handles listing available models
  1049. // GET /v1/models
  1050. // Returns models based on account configurations (model_mapping whitelist)
  1051. // Falls back to default models if no whitelist is configured
  1052. func (h *GatewayHandler) Models(c *gin.Context) {
  1053. apiKey, _ := middleware2.GetAPIKeyFromContext(c)
  1054. var groupID *int64
  1055. var platform string
  1056. if apiKey != nil && apiKey.Group != nil {
  1057. groupID = &apiKey.Group.ID
  1058. platform = apiKey.Group.Platform
  1059. }
  1060. if forcedPlatform, ok := middleware2.GetForcePlatformFromContext(c); ok && strings.TrimSpace(forcedPlatform) != "" {
  1061. platform = forcedPlatform
  1062. }
  1063. if platform == service.PlatformOpenAI && apiKey != nil && apiKey.Group != nil &&
  1064. apiKey.Group.Platform == service.PlatformOpenAI && apiKey.Group.CodexModelsManifestConfig.Enabled {
  1065. h.pinnedOpenAIModels(c, apiKey.Group)
  1066. return
  1067. }
  1068. if platform == service.PlatformComposite {
  1069. availableModels := h.compositeAvailableModels(c.Request.Context(), groupID)
  1070. if apiKey != nil && apiKey.Group != nil && apiKey.Group.ModelAllowlistEnabled() {
  1071. source := availableModels
  1072. if len(source) == 0 {
  1073. source = defaultModelIDsForPlatform(service.PlatformComposite)
  1074. }
  1075. writeAllowlistedModelsList(c, service.PlatformComposite, apiKey.Group.ModelAllowlist.FilterForListing(source))
  1076. return
  1077. }
  1078. if len(availableModels) > 0 {
  1079. writeModelsList(c, service.PlatformComposite, availableModels)
  1080. return
  1081. }
  1082. writeModelsList(c, service.PlatformComposite, defaultModelIDsForPlatform(service.PlatformComposite))
  1083. return
  1084. }
  1085. // Get available models from account configurations for the selected group platform.
  1086. availableModels := h.gatewayService.GetAvailableModels(c.Request.Context(), groupID, platform)
  1087. if apiKey != nil && apiKey.Group != nil && apiKey.Group.ModelAllowlistEnabled() {
  1088. source := modelListingSource(platform, availableModels, defaultModelIDsForPlatform(platform))
  1089. writeAllowlistedModelsList(c, platform, apiKey.Group.ModelAllowlist.FilterForListing(source))
  1090. return
  1091. }
  1092. if len(availableModels) > 0 {
  1093. writeModelsList(c, platform, availableModels)
  1094. return
  1095. }
  1096. // Fallback to default models
  1097. if platform == service.PlatformOpenAI {
  1098. c.JSON(http.StatusOK, gin.H{
  1099. "object": "list",
  1100. "data": openai.DefaultModels,
  1101. })
  1102. return
  1103. }
  1104. if platform == service.PlatformGemini {
  1105. c.JSON(http.StatusOK, gin.H{
  1106. "object": "list",
  1107. "data": geminicli.DefaultModels,
  1108. })
  1109. return
  1110. }
  1111. if platform == service.PlatformGrok {
  1112. writeGrokModelsList(c, xai.DefaultModelIDs())
  1113. return
  1114. }
  1115. c.JSON(http.StatusOK, gin.H{
  1116. "object": "list",
  1117. "data": claude.DefaultModels,
  1118. })
  1119. }
  1120. // CodexModels returns the effective group model list using the manifest shape
  1121. // expected by Codex custom providers. Official OpenAI groups continue to use
  1122. // OpenAIGatewayHandler.CodexModels so their live upstream metadata is preserved.
  1123. func (h *GatewayHandler) CodexModels(c *gin.Context) {
  1124. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  1125. if !ok || apiKey == nil || apiKey.Group == nil {
  1126. h.errorResponse(c, http.StatusUnauthorized, "invalid_request_error", "API key group is required")
  1127. return
  1128. }
  1129. forcedPlatform := ""
  1130. if value, exists := middleware2.GetForcePlatformFromContext(c); exists {
  1131. forcedPlatform = strings.TrimSpace(value)
  1132. }
  1133. modelIDs := h.codexModelIDsForGroup(c.Request.Context(), apiKey.Group, forcedPlatform)
  1134. modelIDs = service.FilterCodexModelIDsForGroup(modelIDs, apiKey.Group)
  1135. body, err := h.gatewayService.BuildCodexModelsManifestForGroup(
  1136. c.Request.Context(),
  1137. apiKey.Group,
  1138. forcedPlatform,
  1139. modelIDs,
  1140. )
  1141. if err != nil {
  1142. h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to build Codex models manifest")
  1143. return
  1144. }
  1145. etag := service.CodexModelsManifestETag(body)
  1146. c.Header("ETag", etag)
  1147. if service.CodexModelsManifestETagMatches(c.GetHeader("If-None-Match"), etag) {
  1148. c.Status(http.StatusNotModified)
  1149. c.Writer.WriteHeaderNow()
  1150. return
  1151. }
  1152. c.Data(http.StatusOK, "application/json", body)
  1153. }
  1154. func (h *GatewayHandler) codexModelIDsForGroup(ctx context.Context, group *service.Group, platformOverride string) []string {
  1155. if h == nil || h.gatewayService == nil || group == nil {
  1156. return nil
  1157. }
  1158. groupID := &group.ID
  1159. platform := strings.TrimSpace(platformOverride)
  1160. if platform == "" {
  1161. platform = group.Platform
  1162. }
  1163. if platform == service.PlatformComposite {
  1164. availableModels := h.compositeAvailableModels(ctx, groupID)
  1165. fallbackModels := defaultCodexModelIDsForPlatform(service.PlatformComposite)
  1166. if group.ModelAllowlistEnabled() {
  1167. source := availableModels
  1168. if len(source) == 0 {
  1169. source = fallbackModels
  1170. }
  1171. return group.ModelAllowlist.FilterForListing(source)
  1172. }
  1173. if len(availableModels) > 0 {
  1174. return availableModels
  1175. }
  1176. return fallbackModels
  1177. }
  1178. availableModels := h.gatewayService.GetAvailableModels(ctx, groupID, platform)
  1179. fallbackModels := defaultCodexModelIDsForPlatform(platform)
  1180. if group.ModelAllowlistEnabled() {
  1181. return group.ModelAllowlist.FilterForListing(modelListingSource(platform, availableModels, fallbackModels))
  1182. }
  1183. if len(availableModels) > 0 {
  1184. return availableModels
  1185. }
  1186. return fallbackModels
  1187. }
  1188. func (h *GatewayHandler) compositeAvailableModels(ctx context.Context, groupID *int64) []string {
  1189. if h == nil || h.gatewayService == nil {
  1190. return nil
  1191. }
  1192. seen := make(map[string]struct{})
  1193. models := make([]string, 0)
  1194. schedulablePlatforms := h.gatewayService.GetSchedulablePlatforms(ctx, groupID)
  1195. for _, platform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformAntigravity, service.PlatformGrok, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax} {
  1196. platformModels := h.gatewayService.GetAvailableModels(ctx, groupID, platform)
  1197. if len(platformModels) == 0 {
  1198. // CN 供应商没有静态默认模型列表(defaultModelIDsForPlatform 的
  1199. // default 分支是 Claude 列表),composite 下只暴露账号映射键。
  1200. if _, ok := schedulablePlatforms[platform]; ok && !service.IsCNProvider(platform) {
  1201. platformModels = defaultModelIDsForPlatform(platform)
  1202. }
  1203. }
  1204. for _, model := range platformModels {
  1205. model = strings.TrimSpace(model)
  1206. if model == "" {
  1207. continue
  1208. }
  1209. if _, ok := seen[model]; ok {
  1210. continue
  1211. }
  1212. seen[model] = struct{}{}
  1213. models = append(models, model)
  1214. }
  1215. }
  1216. return models
  1217. }
  1218. func writeModelsList(c *gin.Context, platform string, modelIDs []string) {
  1219. if platform == service.PlatformOpenAI {
  1220. writeOpenAIModelsList(c, modelIDs)
  1221. return
  1222. }
  1223. if platform == service.PlatformGrok {
  1224. writeGrokModelsList(c, modelIDs)
  1225. return
  1226. }
  1227. models := make([]claude.Model, 0, len(modelIDs))
  1228. for _, modelID := range modelIDs {
  1229. models = append(models, claude.Model{
  1230. ID: modelID,
  1231. Type: "model",
  1232. DisplayName: modelID,
  1233. CreatedAt: "2024-01-01T00:00:00Z",
  1234. })
  1235. }
  1236. c.JSON(http.StatusOK, gin.H{
  1237. "object": "list",
  1238. "data": models,
  1239. })
  1240. }
  1241. func writeAllowlistedModelsList(c *gin.Context, platform string, modelIDs []string) {
  1242. if platform == service.PlatformOpenAI {
  1243. writeOpenAIModelsList(c, modelIDs)
  1244. return
  1245. }
  1246. writeModelsList(c, platform, modelIDs)
  1247. }
  1248. type grokReasoningEffortOption struct {
  1249. Value string `json:"value"`
  1250. Label string `json:"label"`
  1251. Default bool `json:"default,omitempty"`
  1252. }
  1253. type grokModelListItem struct {
  1254. xai.Model
  1255. SupportsReasoningEffort bool `json:"supportsReasoningEffort,omitempty"`
  1256. ReasoningEffort string `json:"reasoningEffort,omitempty"`
  1257. ReasoningEfforts []grokReasoningEffortOption `json:"reasoningEfforts,omitempty"`
  1258. }
  1259. func writeGrokModelsList(c *gin.Context, modelIDs []string) {
  1260. defaults := xai.DefaultModels()
  1261. defaultsByID := make(map[string]xai.Model, len(defaults))
  1262. for _, model := range defaults {
  1263. defaultsByID[model.ID] = model
  1264. }
  1265. models := make([]grokModelListItem, 0, len(modelIDs))
  1266. for _, modelID := range modelIDs {
  1267. model, ok := defaultsByID[modelID]
  1268. if !ok {
  1269. model = xai.Model{
  1270. ID: modelID,
  1271. Object: "model",
  1272. OwnedBy: "xai",
  1273. DisplayName: modelID,
  1274. }
  1275. }
  1276. item := grokModelListItem{Model: model}
  1277. if grokModelSupportsConfigurableReasoning(modelID) {
  1278. item.SupportsReasoningEffort = true
  1279. item.ReasoningEffort = "high"
  1280. efforts := []grokReasoningEffortOption{
  1281. {Value: "low", Label: "Low"},
  1282. {Value: "medium", Label: "Medium"},
  1283. {Value: "high", Label: "High", Default: true},
  1284. }
  1285. if service.GrokSupportsXHighReasoningEffort(modelID) {
  1286. efforts = append(efforts, grokReasoningEffortOption{Value: "xhigh", Label: "xHigh"})
  1287. }
  1288. item.ReasoningEfforts = efforts
  1289. }
  1290. models = append(models, item)
  1291. }
  1292. c.JSON(http.StatusOK, gin.H{
  1293. "object": "list",
  1294. "data": models,
  1295. })
  1296. }
  1297. func grokModelSupportsConfigurableReasoning(modelID string) bool {
  1298. switch strings.ToLower(strings.TrimSpace(modelID)) {
  1299. case "grok-4.6", "grok-4.6-latest", "grok-4.5", "grok-4.5-latest", "grok", "grok-latest", "grok-build", "grok-build-latest", "grok-build-0.1":
  1300. return true
  1301. default:
  1302. return false
  1303. }
  1304. }
  1305. func writeOpenAIModelsList(c *gin.Context, modelIDs []string) {
  1306. defaultsByID := make(map[string]openai.Model, len(openai.DefaultModels))
  1307. for _, model := range openai.DefaultModels {
  1308. defaultsByID[model.ID] = model
  1309. }
  1310. models := make([]openai.Model, 0, len(modelIDs))
  1311. for _, modelID := range modelIDs {
  1312. if model, ok := defaultsByID[modelID]; ok {
  1313. models = append(models, model)
  1314. continue
  1315. }
  1316. models = append(models, openai.Model{
  1317. ID: modelID,
  1318. Object: "model",
  1319. Created: 1704067200,
  1320. OwnedBy: "openai",
  1321. Type: "model",
  1322. DisplayName: modelID,
  1323. })
  1324. }
  1325. c.JSON(http.StatusOK, gin.H{
  1326. "object": "list",
  1327. "data": models,
  1328. })
  1329. }
  1330. // modelListingSource 汇总模型列表过滤的候选来源:账号映射键(availableModels)
  1331. // 与平台默认列表(fallbackModels)。账号映射为空时回落默认列表;Anthropic
  1332. // 平台两者取并集,其余平台以账号映射键为准。
  1333. func modelListingSource(platform string, availableModels, fallbackModels []string) []string {
  1334. if len(availableModels) == 0 {
  1335. return fallbackModels
  1336. }
  1337. if platform == service.PlatformAnthropic {
  1338. return mergeModelIDs(availableModels, fallbackModels)
  1339. }
  1340. return availableModels
  1341. }
  1342. func defaultCodexModelIDsForPlatform(platform string) []string {
  1343. switch platform {
  1344. case service.PlatformDeepseek:
  1345. return []string{"deepseek-v4-pro", "deepseek-v4-flash"}
  1346. case service.PlatformMiniMax:
  1347. return []string{"MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"}
  1348. default:
  1349. return defaultModelIDsForPlatform(platform)
  1350. }
  1351. }
  1352. func defaultModelIDsForPlatform(platform string) []string {
  1353. switch platform {
  1354. case service.PlatformOpenAI:
  1355. return openai.DefaultModelIDs()
  1356. case service.PlatformGemini:
  1357. ids := make([]string, 0, len(geminicli.DefaultModels))
  1358. for _, model := range geminicli.DefaultModels {
  1359. ids = append(ids, model.ID)
  1360. }
  1361. return ids
  1362. case service.PlatformAntigravity:
  1363. models := antigravity.DefaultModels()
  1364. ids := make([]string, 0, len(models))
  1365. for _, model := range models {
  1366. ids = append(ids, model.ID)
  1367. }
  1368. return ids
  1369. case service.PlatformAnthropic:
  1370. return claude.DefaultModelIDs()
  1371. case service.PlatformGrok:
  1372. return xai.DefaultModelIDs()
  1373. case service.PlatformComposite:
  1374. ids := make([]string, 0)
  1375. seen := make(map[string]struct{})
  1376. for _, concretePlatform := range []string{service.PlatformAnthropic, service.PlatformGemini, service.PlatformOpenAI, service.PlatformAntigravity, service.PlatformGrok, service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax} {
  1377. for _, id := range defaultModelIDsForPlatform(concretePlatform) {
  1378. if _, ok := seen[id]; ok {
  1379. continue
  1380. }
  1381. seen[id] = struct{}{}
  1382. ids = append(ids, id)
  1383. }
  1384. }
  1385. return ids
  1386. default:
  1387. ids := make([]string, 0, len(claude.DefaultModels))
  1388. for _, model := range claude.DefaultModels {
  1389. ids = append(ids, model.ID)
  1390. }
  1391. return ids
  1392. }
  1393. }
  1394. func mergeModelIDs(primary, secondary []string) []string {
  1395. seen := make(map[string]struct{}, len(primary)+len(secondary))
  1396. merged := make([]string, 0, len(primary)+len(secondary))
  1397. for _, models := range [][]string{primary, secondary} {
  1398. for _, model := range models {
  1399. model = strings.TrimSpace(model)
  1400. if model == "" {
  1401. continue
  1402. }
  1403. if _, ok := seen[model]; ok {
  1404. continue
  1405. }
  1406. seen[model] = struct{}{}
  1407. merged = append(merged, model)
  1408. }
  1409. }
  1410. return merged
  1411. }
  1412. // AntigravityModels 返回 Antigravity 支持的全部模型
  1413. // GET /antigravity/models
  1414. // 分组级模型白名单开启时按白名单过滤。
  1415. func (h *GatewayHandler) AntigravityModels(c *gin.Context) {
  1416. models := antigravity.DefaultModels()
  1417. if apiKey, ok := middleware2.GetAPIKeyFromContext(c); ok && apiKey != nil && apiKey.Group != nil && apiKey.Group.ModelAllowlistEnabled() {
  1418. filtered := make([]antigravity.ClaudeModel, 0, len(models))
  1419. for _, model := range models {
  1420. if apiKey.Group.ModelAllowlist.Allows(model.ID) {
  1421. filtered = append(filtered, model)
  1422. }
  1423. }
  1424. models = filtered
  1425. }
  1426. c.JSON(http.StatusOK, gin.H{
  1427. "object": "list",
  1428. "data": models,
  1429. })
  1430. }
  1431. func cloneAPIKeyWithGroup(apiKey *service.APIKey, group *service.Group) *service.APIKey {
  1432. if apiKey == nil || group == nil {
  1433. return apiKey
  1434. }
  1435. cloned := *apiKey
  1436. groupID := group.ID
  1437. cloned.GroupID = &groupID
  1438. cloned.Group = group
  1439. return &cloned
  1440. }
  1441. // Usage handles getting account balance and usage statistics for CC Switch integration
  1442. // GET /v1/usage
  1443. //
  1444. // Two modes:
  1445. // - quota_limited: API Key has quota or rate limits configured. Returns key-level limits/usage.
  1446. // - unrestricted: No key-level limits. Returns subscription or wallet balance info.
  1447. func (h *GatewayHandler) Usage(c *gin.Context) {
  1448. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  1449. if !ok {
  1450. h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  1451. return
  1452. }
  1453. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  1454. if !ok {
  1455. h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  1456. return
  1457. }
  1458. ctx := c.Request.Context()
  1459. // 解析可选的日期范围参数(用于 model_stats 查询)
  1460. startTime, endTime := h.parseUsageDateRange(c)
  1461. days, ok := parseAPIKeyDailyUsageDays(c.DefaultQuery("days", ""))
  1462. if !ok {
  1463. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Invalid days, allowed range is 1-90")
  1464. return
  1465. }
  1466. // Best-effort: 获取用量统计(按当前 API Key 过滤),失败不影响基础响应
  1467. usageData := h.buildUsageData(ctx, apiKey.ID)
  1468. dailyUsage := h.buildAPIKeyDailyUsage(c, subject.UserID, apiKey.ID, days)
  1469. // Best-effort: 获取模型统计
  1470. var modelStats any
  1471. if h.usageService != nil {
  1472. if stats, err := h.usageService.GetAPIKeyModelStats(ctx, apiKey.ID, startTime, endTime); err == nil && len(stats) > 0 {
  1473. modelStats = stats
  1474. }
  1475. }
  1476. // 判断模式: key 有总额度或速率限制 → quota_limited,否则 → unrestricted
  1477. isQuotaLimited := apiKey.Quota > 0 || apiKey.HasRateLimits()
  1478. if isQuotaLimited {
  1479. h.usageQuotaLimited(c, ctx, apiKey, usageData, dailyUsage, modelStats)
  1480. return
  1481. }
  1482. h.usageUnrestricted(c, ctx, apiKey, subject, usageData, dailyUsage, modelStats)
  1483. }
  1484. // parseUsageDateRange 解析 start_date / end_date query params,默认返回近 30 天范围
  1485. func (h *GatewayHandler) parseUsageDateRange(c *gin.Context) (time.Time, time.Time) {
  1486. now := timezone.Now()
  1487. endTime := now
  1488. startTime := now.AddDate(0, 0, -30)
  1489. if s := c.Query("start_date"); s != "" {
  1490. if t, err := timezone.ParseInLocation("2006-01-02", s); err == nil {
  1491. startTime = t
  1492. }
  1493. }
  1494. if s := c.Query("end_date"); s != "" {
  1495. if t, err := timezone.ParseInLocation("2006-01-02", s); err == nil {
  1496. endTime = t.AddDate(0, 0, 1) // half-open range upper bound
  1497. }
  1498. }
  1499. return startTime, endTime
  1500. }
  1501. // buildUsageData 构建 today/total 用量摘要
  1502. func (h *GatewayHandler) buildUsageData(ctx context.Context, apiKeyID int64) gin.H {
  1503. if h.usageService == nil {
  1504. return nil
  1505. }
  1506. dashStats, err := h.usageService.GetAPIKeyDashboardStats(ctx, apiKeyID)
  1507. if err != nil || dashStats == nil {
  1508. return nil
  1509. }
  1510. return gin.H{
  1511. "today": gin.H{
  1512. "requests": dashStats.TodayRequests,
  1513. "input_tokens": dashStats.TodayInputTokens,
  1514. "output_tokens": dashStats.TodayOutputTokens,
  1515. "cache_creation_tokens": dashStats.TodayCacheCreationTokens,
  1516. "cache_read_tokens": dashStats.TodayCacheReadTokens,
  1517. "total_tokens": dashStats.TodayTokens,
  1518. "cost": dashStats.TodayCost,
  1519. "actual_cost": dashStats.TodayActualCost,
  1520. },
  1521. "total": gin.H{
  1522. "requests": dashStats.TotalRequests,
  1523. "input_tokens": dashStats.TotalInputTokens,
  1524. "output_tokens": dashStats.TotalOutputTokens,
  1525. "cache_creation_tokens": dashStats.TotalCacheCreationTokens,
  1526. "cache_read_tokens": dashStats.TotalCacheReadTokens,
  1527. "total_tokens": dashStats.TotalTokens,
  1528. "cost": dashStats.TotalCost,
  1529. "actual_cost": dashStats.TotalActualCost,
  1530. },
  1531. "average_duration_ms": dashStats.AverageDurationMs,
  1532. "rpm": dashStats.Rpm,
  1533. "tpm": dashStats.Tpm,
  1534. }
  1535. }
  1536. func (h *GatewayHandler) buildAPIKeyDailyUsage(c *gin.Context, userID, apiKeyID int64, days int) any {
  1537. if h.usageService == nil {
  1538. return nil
  1539. }
  1540. startTime, endTime := apiKeyDailyUsageRange(days, c.Query("timezone"))
  1541. stats, err := h.usageService.GetAPIKeyDailyUsage(c.Request.Context(), userID, apiKeyID, startTime, endTime)
  1542. if err != nil {
  1543. return nil
  1544. }
  1545. return stats
  1546. }
  1547. // usageQuotaLimited 处理 quota_limited 模式的响应
  1548. func (h *GatewayHandler) usageQuotaLimited(c *gin.Context, ctx context.Context, apiKey *service.APIKey, usageData gin.H, dailyUsage any, modelStats any) {
  1549. resp := gin.H{
  1550. "mode": "quota_limited",
  1551. "isValid": apiKey.Status == service.StatusAPIKeyActive || apiKey.Status == service.StatusAPIKeyQuotaExhausted || apiKey.Status == service.StatusAPIKeyExpired,
  1552. "status": apiKey.Status,
  1553. }
  1554. // 总额度信息
  1555. if apiKey.Quota > 0 {
  1556. remaining := apiKey.GetQuotaRemaining()
  1557. resp["quota"] = gin.H{
  1558. "limit": apiKey.Quota,
  1559. "used": apiKey.QuotaUsed,
  1560. "remaining": remaining,
  1561. "unit": "USD",
  1562. }
  1563. resp["remaining"] = remaining
  1564. resp["unit"] = "USD"
  1565. }
  1566. // 速率限制信息(从 DB 获取实时用量)
  1567. if apiKey.HasRateLimits() && h.apiKeyService != nil {
  1568. rateLimitData, err := h.apiKeyService.GetRateLimitData(ctx, apiKey.ID)
  1569. if err == nil && rateLimitData != nil {
  1570. var rateLimits []gin.H
  1571. if apiKey.RateLimit5h > 0 {
  1572. used := rateLimitData.EffectiveUsage5h()
  1573. entry := gin.H{
  1574. "window": "5h",
  1575. "limit": apiKey.RateLimit5h,
  1576. "used": used,
  1577. "remaining": max(0, apiKey.RateLimit5h-used),
  1578. "window_start": rateLimitData.Window5hStart,
  1579. }
  1580. if rateLimitData.Window5hStart != nil && !service.IsWindowExpired(rateLimitData.Window5hStart, service.RateLimitWindow5h) {
  1581. entry["reset_at"] = rateLimitData.Window5hStart.Add(service.RateLimitWindow5h)
  1582. }
  1583. rateLimits = append(rateLimits, entry)
  1584. }
  1585. if apiKey.RateLimit1d > 0 {
  1586. used := rateLimitData.EffectiveUsage1d()
  1587. entry := gin.H{
  1588. "window": "1d",
  1589. "limit": apiKey.RateLimit1d,
  1590. "used": used,
  1591. "remaining": max(0, apiKey.RateLimit1d-used),
  1592. "window_start": rateLimitData.Window1dStart,
  1593. }
  1594. if rateLimitData.Window1dStart != nil && !service.IsWindowExpired(rateLimitData.Window1dStart, service.RateLimitWindow1d) {
  1595. entry["reset_at"] = rateLimitData.Window1dStart.Add(service.RateLimitWindow1d)
  1596. }
  1597. rateLimits = append(rateLimits, entry)
  1598. }
  1599. if apiKey.RateLimit7d > 0 {
  1600. used := rateLimitData.EffectiveUsage7d()
  1601. entry := gin.H{
  1602. "window": "7d",
  1603. "limit": apiKey.RateLimit7d,
  1604. "used": used,
  1605. "remaining": max(0, apiKey.RateLimit7d-used),
  1606. "window_start": rateLimitData.Window7dStart,
  1607. }
  1608. if rateLimitData.Window7dStart != nil && !service.IsWindowExpired(rateLimitData.Window7dStart, service.RateLimitWindow7d) {
  1609. entry["reset_at"] = rateLimitData.Window7dStart.Add(service.RateLimitWindow7d)
  1610. }
  1611. rateLimits = append(rateLimits, entry)
  1612. }
  1613. if len(rateLimits) > 0 {
  1614. resp["rate_limits"] = rateLimits
  1615. }
  1616. }
  1617. }
  1618. // 过期时间
  1619. if apiKey.ExpiresAt != nil {
  1620. resp["expires_at"] = apiKey.ExpiresAt
  1621. resp["days_until_expiry"] = apiKey.GetDaysUntilExpiry()
  1622. }
  1623. if usageData != nil {
  1624. resp["usage"] = usageData
  1625. }
  1626. if dailyUsage != nil {
  1627. resp["daily_usage"] = dailyUsage
  1628. }
  1629. if modelStats != nil {
  1630. resp["model_stats"] = modelStats
  1631. }
  1632. c.JSON(http.StatusOK, resp)
  1633. }
  1634. // usageUnrestricted 处理 unrestricted 模式的响应(向后兼容)
  1635. func (h *GatewayHandler) usageUnrestricted(c *gin.Context, ctx context.Context, apiKey *service.APIKey, subject middleware2.AuthSubject, usageData gin.H, dailyUsage any, modelStats any) {
  1636. // 订阅模式
  1637. if apiKey.Group != nil && apiKey.Group.IsSubscriptionType() {
  1638. resp := gin.H{
  1639. "mode": "unrestricted",
  1640. "isValid": true,
  1641. "planName": apiKey.Group.Name,
  1642. "unit": "USD",
  1643. }
  1644. // 订阅信息可能不在 context 中(/v1/usage 路径跳过了中间件的计费检查)
  1645. subscription, ok := middleware2.GetSubscriptionFromContext(c)
  1646. if ok {
  1647. remaining := h.calculateSubscriptionRemaining(apiKey.Group, subscription)
  1648. resp["remaining"] = remaining
  1649. resp["subscription"] = gin.H{
  1650. "daily_usage_usd": subscription.DailyUsageUSD,
  1651. "weekly_usage_usd": subscription.WeeklyUsageUSD,
  1652. "monthly_usage_usd": subscription.MonthlyUsageUSD,
  1653. "daily_limit_usd": apiKey.Group.DailyLimitUSD,
  1654. "weekly_limit_usd": apiKey.Group.WeeklyLimitUSD,
  1655. "monthly_limit_usd": apiKey.Group.MonthlyLimitUSD,
  1656. "weekly_window_start": subscription.WeeklyWindowStart,
  1657. "expires_at": subscription.ExpiresAt,
  1658. }
  1659. }
  1660. if usageData != nil {
  1661. resp["usage"] = usageData
  1662. }
  1663. if dailyUsage != nil {
  1664. resp["daily_usage"] = dailyUsage
  1665. }
  1666. if modelStats != nil {
  1667. resp["model_stats"] = modelStats
  1668. }
  1669. c.JSON(http.StatusOK, resp)
  1670. return
  1671. }
  1672. // 余额模式
  1673. latestUser, err := h.userService.GetByID(ctx, subject.UserID)
  1674. if err != nil {
  1675. h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to get user info")
  1676. return
  1677. }
  1678. resp := gin.H{
  1679. "mode": "unrestricted",
  1680. "isValid": true,
  1681. "planName": "钱包余额",
  1682. "remaining": latestUser.Balance,
  1683. "unit": "USD",
  1684. "balance": latestUser.Balance,
  1685. }
  1686. if usageData != nil {
  1687. resp["usage"] = usageData
  1688. }
  1689. if dailyUsage != nil {
  1690. resp["daily_usage"] = dailyUsage
  1691. }
  1692. if modelStats != nil {
  1693. resp["model_stats"] = modelStats
  1694. }
  1695. c.JSON(http.StatusOK, resp)
  1696. }
  1697. // calculateSubscriptionRemaining 计算订阅剩余可用额度
  1698. // 逻辑:
  1699. // 1. 如果日/周/月任一限额达到100%,返回0
  1700. // 2. 否则返回所有已配置周期中剩余额度的最小值
  1701. func (h *GatewayHandler) calculateSubscriptionRemaining(group *service.Group, sub *service.UserSubscription) float64 {
  1702. var remainingValues []float64
  1703. // 检查日限额
  1704. if group.HasDailyLimit() {
  1705. remaining := *group.DailyLimitUSD - sub.DailyUsageUSD
  1706. if remaining <= 0 {
  1707. return 0
  1708. }
  1709. remainingValues = append(remainingValues, remaining)
  1710. }
  1711. // 检查周限额
  1712. if group.HasWeeklyLimit() {
  1713. remaining := *group.WeeklyLimitUSD - sub.WeeklyUsageUSD
  1714. if remaining <= 0 {
  1715. return 0
  1716. }
  1717. remainingValues = append(remainingValues, remaining)
  1718. }
  1719. // 检查月限额
  1720. if group.HasMonthlyLimit() {
  1721. remaining := *group.MonthlyLimitUSD - sub.MonthlyUsageUSD
  1722. if remaining <= 0 {
  1723. return 0
  1724. }
  1725. remainingValues = append(remainingValues, remaining)
  1726. }
  1727. // 如果没有配置任何限额,返回-1表示无限制
  1728. if len(remainingValues) == 0 {
  1729. return -1
  1730. }
  1731. // 返回最小值
  1732. min := remainingValues[0]
  1733. for _, v := range remainingValues[1:] {
  1734. if v < min {
  1735. min = v
  1736. }
  1737. }
  1738. return min
  1739. }
  1740. // handleConcurrencyError handles concurrency-related acquire errors.
  1741. func (h *GatewayHandler) handleConcurrencyError(c *gin.Context, err error, slotType string, streamStarted bool) {
  1742. status, errType, code, message := concurrencyErrorResponse(err, slotType)
  1743. h.handleStreamingAwareErrorWithCode(c, status, errType, code, message, streamStarted)
  1744. }
  1745. func (h *GatewayHandler) handleFailoverExhausted(c *gin.Context, failoverErr *service.UpstreamFailoverError, platform string, streamStarted bool) {
  1746. if failoverErr != nil && failoverErr.Reason == "account_traffic_limit" {
  1747. copyFailoverRetryAfter(c, failoverErr.ResponseHeaders)
  1748. h.handleStreamingAwareError(c, failoverErr.ClientStatusCode, service.AccountTrafficErrorType(failoverErr.ClientStatusCode), failoverErr.ClientMessage, streamStarted)
  1749. return
  1750. }
  1751. statusCode := failoverErr.StatusCode
  1752. responseBody := failoverErr.ResponseBody
  1753. if service.IsOpenAISilentRefusalErrorBody(responseBody) {
  1754. service.SetOpsUpstreamError(c, statusCode, service.OpenAISilentRefusalClientMessage(), "")
  1755. h.handleStreamingAwareError(c, http.StatusBadGateway, "upstream_error", service.OpenAISilentRefusalClientMessage(), streamStarted)
  1756. return
  1757. }
  1758. // 先检查透传规则
  1759. if h.errorPassthroughService != nil && len(responseBody) > 0 {
  1760. if rule := h.errorPassthroughService.MatchRule(platform, statusCode, responseBody); rule != nil {
  1761. // 确定响应状态码
  1762. respCode := statusCode
  1763. if !rule.PassthroughCode && rule.ResponseCode != nil {
  1764. respCode = *rule.ResponseCode
  1765. }
  1766. // 确定响应消息
  1767. msg := service.ExtractUpstreamErrorMessage(responseBody)
  1768. if !rule.PassthroughBody && rule.CustomMessage != nil {
  1769. msg = *rule.CustomMessage
  1770. }
  1771. if rule.SkipMonitoring {
  1772. c.Set(service.OpsSkipPassthroughKey, true)
  1773. }
  1774. h.handleStreamingAwareError(c, respCode, "upstream_error", msg, streamStarted)
  1775. return
  1776. }
  1777. }
  1778. // 记录原始上游状态码,以便 ops 错误日志捕获真实的上游错误
  1779. upstreamMsg := service.ExtractUpstreamErrorMessage(responseBody)
  1780. service.SetOpsUpstreamError(c, statusCode, upstreamMsg, "")
  1781. // 使用默认的错误映射
  1782. status, errType, errMsg := h.mapUpstreamError(statusCode)
  1783. h.handleStreamingAwareError(c, status, errType, errMsg, streamStarted)
  1784. }
  1785. // handleFailoverExhaustedSimple 简化版本,用于没有响应体的情况
  1786. func (h *GatewayHandler) handleFailoverExhaustedSimple(c *gin.Context, statusCode int, streamStarted bool) {
  1787. status, errType, errMsg := h.mapUpstreamError(statusCode)
  1788. service.SetOpsUpstreamError(c, statusCode, errMsg, "")
  1789. h.handleStreamingAwareError(c, status, errType, errMsg, streamStarted)
  1790. }
  1791. func (h *GatewayHandler) mapUpstreamError(statusCode int) (int, string, string) {
  1792. switch statusCode {
  1793. case 401:
  1794. return http.StatusBadGateway, "upstream_error", "Upstream authentication failed, please contact administrator"
  1795. case 403:
  1796. return http.StatusBadGateway, "upstream_error", "Upstream access forbidden, please contact administrator"
  1797. case 429:
  1798. return http.StatusTooManyRequests, "rate_limit_error", "Upstream rate limit exceeded, please retry later"
  1799. case 529:
  1800. return http.StatusServiceUnavailable, "overloaded_error", "Upstream service overloaded, please retry later"
  1801. case 500, 502, 503, 504:
  1802. return http.StatusBadGateway, "upstream_error", "Upstream service temporarily unavailable"
  1803. default:
  1804. return http.StatusBadGateway, "upstream_error", "Upstream request failed"
  1805. }
  1806. }
  1807. // handleStreamingAwareError handles errors that may occur after streaming has started
  1808. func (h *GatewayHandler) handleStreamingAwareError(c *gin.Context, status int, errType, message string, streamStarted bool) {
  1809. h.handleStreamingAwareErrorWithCode(c, status, errType, "", message, streamStarted)
  1810. }
  1811. func (h *GatewayHandler) handleStreamingAwareErrorWithCode(c *gin.Context, status int, errType, code, message string, streamStarted bool) {
  1812. if streamStarted {
  1813. // 响应状态码已固化为 200(ping/部分数据已 flush),错误只能就地以 SSE 帧回传。
  1814. // 标记本次流内错误,供 ops_error_logger 补记——否则该中间件按 status>=400 采集,
  1815. // 这类挂在 200 流上的失败(如并发限流回退)不会进错误看板。
  1816. service.MarkOpsStreamError(c, errType, message, status)
  1817. // /v1/responses 的严格 SDK(Codex CLI)要求终止事件必须属于
  1818. // response.completed/failed/incomplete/cancelled 集合。
  1819. // Anthropic-backed Responses 路径同样会因为通用 error 帧被拒。
  1820. if inboundIsResponses(c) {
  1821. if writeResponsesFailedSSE(c, errType, code, message) {
  1822. return
  1823. }
  1824. }
  1825. // Stream already started, send error as SSE event then close
  1826. flusher, ok := c.Writer.(http.Flusher)
  1827. if ok {
  1828. // SSE 错误事件固定 schema,使用 Quote 直拼可避免额外 Marshal 分配。
  1829. errorCode := ""
  1830. if code != "" {
  1831. errorCode = `,"code":` + strconv.Quote(code)
  1832. }
  1833. errorEvent := `data: {"type":"error","error":{"type":` + strconv.Quote(errType) + errorCode + `,"message":` + strconv.Quote(message) + `}}` + "\n\n"
  1834. if _, err := fmt.Fprint(c.Writer, errorEvent); err != nil {
  1835. _ = c.Error(err)
  1836. }
  1837. flusher.Flush()
  1838. }
  1839. return
  1840. }
  1841. // Normal case: return JSON response with proper status code
  1842. h.errorResponseWithCode(c, status, errType, code, message)
  1843. }
  1844. // ensureForwardErrorResponse 在 Forward 返回错误但尚未写响应时补写统一错误响应。
  1845. // Writer 已被写过时(ping 已 flush)走 streamStarted 分支,
  1846. // 让 handleStreamingAwareError 通过 SSE 发协议合规的终止事件,
  1847. // 否则下游收到的就是 silent EOF。
  1848. func (h *GatewayHandler) ensureForwardErrorResponse(c *gin.Context, streamStarted bool) bool {
  1849. if c == nil || c.Writer == nil {
  1850. return false
  1851. }
  1852. if service.IsResponseCommitted(c) {
  1853. return false
  1854. }
  1855. if c.Writer.Written() {
  1856. streamStarted = true
  1857. }
  1858. h.handleStreamingAwareError(c, http.StatusBadGateway, "upstream_error", "Upstream request failed", streamStarted)
  1859. return true
  1860. }
  1861. // gatewayForwardErrorAlreadyCommunicated reports whether a Forward implementation
  1862. // has already written a complete error response to the client before returning
  1863. // an error to the handler.
  1864. //
  1865. // This is intentionally narrower than "writer size changed": a stream may have
  1866. // only emitted keepalive pings or partial data, in which case the handler still
  1867. // needs to append a protocol-level terminal error. Non-SSE output from Forward
  1868. // is different: service-level helpers such as handleErrorResponse/writeClaudeError
  1869. // already wrote the client-visible JSON body, so adding the generic streaming
  1870. // fallback would corrupt the response by appending a second `data: ...` frame.
  1871. func gatewayForwardErrorAlreadyCommunicated(c *gin.Context, writerSizeBeforeForward int, err error) bool {
  1872. if err == nil || c == nil || c.Writer == nil {
  1873. return false
  1874. }
  1875. if c.Writer.Size() == writerSizeBeforeForward {
  1876. return false
  1877. }
  1878. contentType := strings.ToLower(strings.TrimSpace(c.Writer.Header().Get("Content-Type")))
  1879. if contentType == "" {
  1880. return false
  1881. }
  1882. return !strings.Contains(contentType, "text/event-stream")
  1883. }
  1884. // checkClaudeCodeVersion 检查 Claude Code 客户端版本是否满足版本要求
  1885. // 仅对已识别的 Claude Code 客户端执行,count_tokens 路径除外
  1886. func (h *GatewayHandler) checkClaudeCodeVersion(c *gin.Context) bool {
  1887. ctx := c.Request.Context()
  1888. if !service.IsClaudeCodeClient(ctx) {
  1889. return true
  1890. }
  1891. // 排除 count_tokens 子路径
  1892. if strings.HasSuffix(c.Request.URL.Path, "/count_tokens") {
  1893. return true
  1894. }
  1895. minVersion, maxVersion := h.settingService.GetClaudeCodeVersionBounds(ctx)
  1896. if minVersion == "" && maxVersion == "" {
  1897. return true // 未设置,不检查
  1898. }
  1899. clientVersion := service.GetClaudeCodeVersion(ctx)
  1900. if clientVersion == "" {
  1901. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error",
  1902. "Unable to determine Claude Code version. Please update Claude Code: npm update -g @anthropic-ai/claude-code")
  1903. return false
  1904. }
  1905. if minVersion != "" && service.CompareVersions(clientVersion, minVersion) < 0 {
  1906. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error",
  1907. fmt.Sprintf("Your Claude Code version (%s) is below the minimum required version (%s). Please update: npm update -g @anthropic-ai/claude-code",
  1908. clientVersion, minVersion))
  1909. return false
  1910. }
  1911. if maxVersion != "" && service.CompareVersions(clientVersion, maxVersion) > 0 {
  1912. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error",
  1913. fmt.Sprintf("Your Claude Code version (%s) exceeds the maximum allowed version (%s). "+
  1914. "Please downgrade: npm install -g @anthropic-ai/claude-code@%s && "+
  1915. "set CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 to prevent auto-upgrade",
  1916. clientVersion, maxVersion, maxVersion))
  1917. return false
  1918. }
  1919. return true
  1920. }
  1921. // errorResponse 返回Claude API格式的错误响应
  1922. func (h *GatewayHandler) errorResponse(c *gin.Context, status int, errType, message string) {
  1923. h.errorResponseWithCode(c, status, errType, "", message)
  1924. }
  1925. func (h *GatewayHandler) errorResponseWithCode(c *gin.Context, status int, errType, code, message string) {
  1926. errorObject := gin.H{"type": errType, "message": message}
  1927. if code != "" {
  1928. errorObject["code"] = code
  1929. }
  1930. c.JSON(status, gin.H{
  1931. "type": "error",
  1932. "error": errorObject,
  1933. })
  1934. }
  1935. // CountTokens handles token counting endpoint
  1936. // POST /v1/messages/count_tokens
  1937. // 特点:校验订阅/余额,但不计算并发、不记录使用量
  1938. func (h *GatewayHandler) CountTokens(c *gin.Context) {
  1939. // 从context获取apiKey和user(ApiKeyAuth中间件已设置)
  1940. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  1941. if !ok {
  1942. h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  1943. return
  1944. }
  1945. _, ok = middleware2.GetAuthSubjectFromContext(c)
  1946. if !ok {
  1947. h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  1948. return
  1949. }
  1950. reqLog := requestLogger(
  1951. c,
  1952. "handler.gateway.count_tokens",
  1953. zap.Int64("api_key_id", apiKey.ID),
  1954. zap.Any("group_id", apiKey.GroupID),
  1955. )
  1956. defer h.maybeLogCompatibilityFallbackMetrics(reqLog)
  1957. // 读取请求体
  1958. body, err := readLenientJSONRequestBodyWithPrealloc(c.Request, h.cfg)
  1959. if err != nil {
  1960. if maxErr, ok := extractMaxBytesError(err); ok {
  1961. h.errorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
  1962. return
  1963. }
  1964. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
  1965. return
  1966. }
  1967. if len(body) == 0 {
  1968. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
  1969. return
  1970. }
  1971. setOpsRequestContext(c, "", false)
  1972. bodyRef := service.NewRequestBodyRef(body)
  1973. parsedReq, err := service.ParseGatewayRequest(bodyRef, domain.PlatformAnthropic)
  1974. if err != nil {
  1975. logRequestBodyParseFailure(reqLog, body, err)
  1976. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  1977. return
  1978. }
  1979. body = parsedReq.Body.Bytes()
  1980. // count_tokens 走 messages 严格校验时,复用已解析请求,避免二次反序列化。
  1981. SetClaudeCodeClientContext(c, body, parsedReq)
  1982. ensureCompositeTargetPlatform(c, apiKey, parsedReq.Model)
  1983. reqLog = reqLog.With(zap.String("model", parsedReq.Model), zap.Bool("stream", parsedReq.Stream))
  1984. // 在请求上下文中记录 thinking 状态,供 Antigravity 最终模型 key 推导/模型维度限流使用
  1985. c.Request = c.Request.WithContext(service.WithThinkingEnabled(c.Request.Context(), parsedReq.ThinkingEnabled, h.metadataBridgeEnabled()))
  1986. // 验证 model 必填
  1987. if parsedReq.Model == "" {
  1988. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  1989. return
  1990. }
  1991. if !compositeTargetPlatformResolved(c, apiKey, parsedReq.Model) {
  1992. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by composite groups")
  1993. return
  1994. }
  1995. setOpsRequestContext(c, parsedReq.Model, parsedReq.Stream)
  1996. setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(parsedReq.Stream, false)))
  1997. // 获取订阅信息(可能为nil)
  1998. subscription, _ := middleware2.GetSubscriptionFromContext(c)
  1999. // 校验 billing eligibility(订阅/余额)
  2000. // 【注意】不计算并发,但需要校验订阅/余额
  2001. if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription, service.QuotaPlatform(c.Request.Context(), apiKey)); err != nil {
  2002. status, code, message, retryAfter := billingErrorDetails(err)
  2003. if retryAfter > 0 {
  2004. c.Header("Retry-After", strconv.Itoa(retryAfter))
  2005. }
  2006. h.errorResponse(c, status, code, message)
  2007. return
  2008. }
  2009. // 计算粘性会话 hash
  2010. parsedReq.SessionContext = &service.SessionContext{
  2011. ClientIP: ip.GetClientIP(c),
  2012. UserAgent: c.GetHeader("User-Agent"),
  2013. APIKeyID: apiKey.ID,
  2014. }
  2015. sessionHash := h.gatewayService.GenerateSessionHash(parsedReq)
  2016. // 选择支持该模型的账号
  2017. account, err := h.gatewayService.SelectAccountForModel(c.Request.Context(), apiKey.GroupID, sessionHash, parsedReq.Model)
  2018. if err != nil {
  2019. reqLog.Warn("gateway.count_tokens_select_account_failed", zap.Error(err))
  2020. cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, parsedReq.Model, parsedReq.Model, service.PlatformAnthropic)
  2021. if !cls.ModelNotFound {
  2022. markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
  2023. }
  2024. h.errorResponse(c, cls.Status, cls.ErrType, cls.Message)
  2025. return
  2026. }
  2027. setOpsSelectedAccount(c, account.ID, account.Platform)
  2028. // 转发请求(不记录使用量)
  2029. if err := h.gatewayService.ForwardCountTokens(c.Request.Context(), c, account, parsedReq); err != nil {
  2030. reqLog.Error("gateway.count_tokens_forward_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  2031. // 错误响应已在 ForwardCountTokens 中处理
  2032. // 上游未服务该会话,立即释放选号时注册的会话槽(客户端可能已断开,用独立 ctx)
  2033. h.gatewayService.ReleaseAccountSession(context.Background(), account, sessionHash)
  2034. return
  2035. }
  2036. }
  2037. // InterceptType 表示请求拦截类型
  2038. type InterceptType int
  2039. const (
  2040. InterceptTypeNone InterceptType = iota
  2041. InterceptTypeWarmup // 预热请求(返回 "New Conversation")
  2042. InterceptTypeSuggestionMode // SUGGESTION MODE(返回空字符串)
  2043. InterceptTypeMaxTokensOneHaiku // max_tokens=1 + haiku 探测请求(返回 "#")
  2044. )
  2045. // isHaikuModel 检查模型名称是否包含 "haiku"(大小写不敏感)
  2046. func isHaikuModel(model string) bool {
  2047. return strings.Contains(strings.ToLower(model), "haiku")
  2048. }
  2049. // isMaxTokensOneHaikuRequest 检查是否为 max_tokens=1 + haiku 模型的探测请求
  2050. // 这类请求用于 Claude Code 验证 API 连通性(流式/非流式均会出现,如 cc-switch v3.9.0 起的健康检查探测为流式)
  2051. // 条件:max_tokens == 1 且 model 包含 "haiku"
  2052. func isMaxTokensOneHaikuRequest(model string, maxTokens int) bool {
  2053. return maxTokens == 1 && isHaikuModel(model)
  2054. }
  2055. // detectInterceptType 检测请求是否需要拦截,返回拦截类型
  2056. // 参数说明:
  2057. // - body: 请求体字节
  2058. // - model: 请求的模型名称
  2059. // - maxTokens: max_tokens 值
  2060. // - isClaudeCodeClient: 是否已通过 Claude Code 客户端校验
  2061. func detectInterceptType(body []byte, model string, maxTokens int, isClaudeCodeClient bool) InterceptType {
  2062. // 优先检查 max_tokens=1 + haiku 探测请求(流式/非流式均适用)
  2063. if isClaudeCodeClient && isMaxTokensOneHaikuRequest(model, maxTokens) {
  2064. return InterceptTypeMaxTokensOneHaiku
  2065. }
  2066. // 快速检查:如果不包含任何关键字,直接返回
  2067. bodyStr := string(body)
  2068. hasSuggestionMode := strings.Contains(bodyStr, "[SUGGESTION MODE:")
  2069. hasWarmupKeyword := strings.Contains(bodyStr, "title") || strings.Contains(bodyStr, "Warmup")
  2070. if !hasSuggestionMode && !hasWarmupKeyword {
  2071. return InterceptTypeNone
  2072. }
  2073. // 解析请求(只解析一次)
  2074. var req struct {
  2075. Messages []struct {
  2076. Role string `json:"role"`
  2077. Content []struct {
  2078. Type string `json:"type"`
  2079. Text string `json:"text"`
  2080. } `json:"content"`
  2081. } `json:"messages"`
  2082. System []struct {
  2083. Text string `json:"text"`
  2084. } `json:"system"`
  2085. }
  2086. if err := json.Unmarshal(body, &req); err != nil {
  2087. return InterceptTypeNone
  2088. }
  2089. // 检查 SUGGESTION MODE(最后一条 user 消息)
  2090. if hasSuggestionMode && len(req.Messages) > 0 {
  2091. lastMsg := req.Messages[len(req.Messages)-1]
  2092. if lastMsg.Role == "user" && len(lastMsg.Content) > 0 &&
  2093. lastMsg.Content[0].Type == "text" &&
  2094. strings.HasPrefix(lastMsg.Content[0].Text, "[SUGGESTION MODE:") {
  2095. return InterceptTypeSuggestionMode
  2096. }
  2097. }
  2098. // 检查 Warmup 请求
  2099. if hasWarmupKeyword {
  2100. // 检查 messages 中的标题提示模式
  2101. for _, msg := range req.Messages {
  2102. for _, content := range msg.Content {
  2103. if content.Type == "text" {
  2104. if strings.Contains(content.Text, "Please write a 5-10 word title for the following conversation:") ||
  2105. content.Text == "Warmup" {
  2106. return InterceptTypeWarmup
  2107. }
  2108. }
  2109. }
  2110. }
  2111. // 检查 system 中的标题提取模式
  2112. for _, sys := range req.System {
  2113. if strings.Contains(sys.Text, "nalyze if this message indicates a new conversation topic. If it does, extract a 2-3 word title") {
  2114. return InterceptTypeWarmup
  2115. }
  2116. }
  2117. }
  2118. return InterceptTypeNone
  2119. }
  2120. // sendMockInterceptStream 发送流式 mock 响应(用于请求拦截)
  2121. func sendMockInterceptStream(c *gin.Context, model string, interceptType InterceptType) {
  2122. c.Header("Content-Type", "text/event-stream")
  2123. c.Header("Cache-Control", "no-cache")
  2124. c.Header("Connection", "keep-alive")
  2125. c.Header("X-Accel-Buffering", "no")
  2126. // 根据拦截类型决定响应内容
  2127. var msgID string
  2128. var outputTokens int
  2129. var textDeltas []string
  2130. switch interceptType {
  2131. case InterceptTypeSuggestionMode:
  2132. msgID = generateRealisticMsgID()
  2133. outputTokens = 1
  2134. textDeltas = []string{""} // 空内容
  2135. default: // InterceptTypeWarmup
  2136. msgID = generateRealisticMsgID()
  2137. outputTokens = 2
  2138. textDeltas = []string{"New", " Conversation"}
  2139. }
  2140. // Build message_start event — field order matches real Anthropic API response.
  2141. messageStartJSON := `{"type":"message_start","message":{"model":` + strconv.Quote(model) + `,"id":` + strconv.Quote(msgID) + `,"type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"stop_details":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0}}}`
  2142. // Build events
  2143. events := []string{
  2144. `event: message_start` + "\n" + `data: ` + string(messageStartJSON),
  2145. `event: content_block_start` + "\n" + `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`,
  2146. }
  2147. // Add text deltas
  2148. for _, text := range textDeltas {
  2149. deltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":` + strconv.Quote(text) + `}}`
  2150. events = append(events, `event: content_block_delta`+"\n"+`data: `+string(deltaJSON))
  2151. }
  2152. // Add final events
  2153. messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null,"stop_details":null},"usage":{"output_tokens":` + strconv.Itoa(outputTokens) + `}}`
  2154. events = append(events,
  2155. `event: content_block_stop`+"\n"+`data: {"index":0,"type":"content_block_stop"}`,
  2156. `event: message_delta`+"\n"+`data: `+string(messageDeltaJSON),
  2157. `event: message_stop`+"\n"+`data: {"type":"message_stop"}`,
  2158. )
  2159. for _, event := range events {
  2160. _, _ = c.Writer.WriteString(event + "\n\n")
  2161. c.Writer.Flush()
  2162. time.Sleep(20 * time.Millisecond)
  2163. }
  2164. }
  2165. // generateRealisticMsgID 生成仿真的消息 ID(msg_01XXXXXXX 格式)
  2166. // 格式与 Anthropic API 官方响应一致:msg_01 + 22 位 Base62 随机字符
  2167. func generateRealisticMsgID() string {
  2168. const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
  2169. const idLen = 22
  2170. randomBytes := make([]byte, idLen)
  2171. if _, err := rand.Read(randomBytes); err != nil {
  2172. return fmt.Sprintf("msg_01%d", time.Now().UnixNano())
  2173. }
  2174. b := make([]byte, idLen)
  2175. for i := range b {
  2176. b[i] = charset[int(randomBytes[i])%len(charset)]
  2177. }
  2178. return "msg_01" + string(b)
  2179. }
  2180. // sendMockInterceptResponse 发送非流式 mock 响应(用于请求拦截)
  2181. func sendMockInterceptResponse(c *gin.Context, model string, interceptType InterceptType) {
  2182. var msgID, text, stopReason string
  2183. var outputTokens int
  2184. switch interceptType {
  2185. case InterceptTypeSuggestionMode:
  2186. msgID = generateRealisticMsgID()
  2187. text = ""
  2188. outputTokens = 1
  2189. stopReason = "end_turn"
  2190. case InterceptTypeMaxTokensOneHaiku:
  2191. msgID = generateRealisticMsgID()
  2192. text = "#"
  2193. outputTokens = 1
  2194. stopReason = "max_tokens" // max_tokens=1 探测请求的 stop_reason 应为 max_tokens
  2195. default: // InterceptTypeWarmup
  2196. msgID = generateRealisticMsgID()
  2197. text = "New Conversation"
  2198. outputTokens = 2
  2199. stopReason = "end_turn"
  2200. }
  2201. // 构建完整的响应格式(与 Anthropic API 官方响应格式一致)
  2202. response := gin.H{
  2203. "model": model,
  2204. "id": msgID,
  2205. "type": "message",
  2206. "role": "assistant",
  2207. "content": []gin.H{{"type": "text", "text": text}},
  2208. "stop_reason": stopReason,
  2209. "stop_sequence": nil,
  2210. "stop_details": nil,
  2211. "usage": gin.H{
  2212. "input_tokens": 10,
  2213. "cache_creation_input_tokens": 0,
  2214. "cache_read_input_tokens": 0,
  2215. "cache_creation": gin.H{
  2216. "ephemeral_5m_input_tokens": 0,
  2217. "ephemeral_1h_input_tokens": 0,
  2218. },
  2219. "output_tokens": outputTokens,
  2220. },
  2221. }
  2222. c.JSON(http.StatusOK, response)
  2223. }
  2224. // extractQuotaResetSeconds 从 quota 错误的 metadata 中提取 window_resets_at 并计算
  2225. // 距重置剩余秒数。fallback 路径必须返回 ≥1 秒,避免客户端立即重试无限循环。
  2226. func extractQuotaResetSeconds(err error) int {
  2227. const fallback = 60
  2228. appErr := pkgerrors.FromError(err)
  2229. if appErr == nil {
  2230. return fallback
  2231. }
  2232. raw, ok := appErr.Metadata["window_resets_at"]
  2233. if !ok || raw == "" {
  2234. return fallback
  2235. }
  2236. resetAt, parseErr := time.Parse(time.RFC3339, raw)
  2237. if parseErr != nil {
  2238. logger.L().With(
  2239. zap.String("component", "handler.gateway.billing"),
  2240. zap.String("raw", raw),
  2241. zap.Error(parseErr),
  2242. ).Warn("quota.invalid_window_resets_at_format")
  2243. return fallback
  2244. }
  2245. secs := time.Until(resetAt).Seconds()
  2246. if secs <= 0 {
  2247. // reset 时间已过:cache 与 DB 应该正在自愈,返回 fallback 让客户端按常规节奏退避,
  2248. // 避免返回 1 秒导致客户端立即重试仍触发限额的退避循环。
  2249. return fallback
  2250. }
  2251. return int(math.Ceil(secs))
  2252. }
  2253. func billingErrorDetails(err error) (status int, code, message string, retryAfter int) {
  2254. if errors.Is(err, service.ErrBillingServiceUnavailable) {
  2255. msg := pkgerrors.Message(err)
  2256. if msg == "" {
  2257. msg = "Billing service temporarily unavailable. Please retry later."
  2258. }
  2259. return http.StatusServiceUnavailable, "billing_service_error", msg, 0
  2260. }
  2261. if errors.Is(err, service.ErrAPIKeyRateLimit5hExceeded) {
  2262. msg := pkgerrors.Message(err)
  2263. return http.StatusTooManyRequests, "rate_limit_exceeded", msg, 0
  2264. }
  2265. if errors.Is(err, service.ErrAPIKeyRateLimit1dExceeded) {
  2266. msg := pkgerrors.Message(err)
  2267. return http.StatusTooManyRequests, "rate_limit_exceeded", msg, 0
  2268. }
  2269. if errors.Is(err, service.ErrAPIKeyRateLimit7dExceeded) {
  2270. msg := pkgerrors.Message(err)
  2271. return http.StatusTooManyRequests, "rate_limit_exceeded", msg, 0
  2272. }
  2273. // 用户/分组 RPM 超限统一映射为 HTTP 429;保留与其它 rate_limit 一致的错误码便于客户端分类。
  2274. // 返回 Retry-After 秒数(当前分钟剩余秒数),让 SDK 自动退避。
  2275. if errors.Is(err, service.ErrGroupRPMExceeded) || errors.Is(err, service.ErrUserRPMExceeded) {
  2276. msg := pkgerrors.Message(err)
  2277. retrySeconds := 60 - int(time.Now().Unix()%60)
  2278. return http.StatusTooManyRequests, "rate_limit_exceeded", msg, retrySeconds
  2279. }
  2280. if errors.Is(err, service.ErrUserPlatformDailyQuotaExhausted) ||
  2281. errors.Is(err, service.ErrUserPlatformWeeklyQuotaExhausted) ||
  2282. errors.Is(err, service.ErrUserPlatformMonthlyQuotaExhausted) {
  2283. // 与 RPM 超限一致映射 429 + Retry-After,让 SDK 自动退避(而非 403 直接失败)。
  2284. // 错误码用 rate_limit_exceeded 与 OpenAI 兼容客户端一致;细分类型由 ErrCode + window_resets_at metadata 区分。
  2285. msg := pkgerrors.Message(err)
  2286. return http.StatusTooManyRequests, "rate_limit_exceeded", msg, extractQuotaResetSeconds(err)
  2287. }
  2288. msg := pkgerrors.Message(err)
  2289. if msg == "" {
  2290. logger.L().With(
  2291. zap.String("component", "handler.gateway.billing"),
  2292. zap.Error(err),
  2293. ).Warn("gateway.billing_error_missing_message")
  2294. msg = "Billing error"
  2295. }
  2296. return http.StatusForbidden, "billing_error", msg, 0
  2297. }
  2298. func (h *GatewayHandler) metadataBridgeEnabled() bool {
  2299. if h == nil || h.cfg == nil {
  2300. return true
  2301. }
  2302. return h.cfg.Gateway.OpenAIWS.MetadataBridgeEnabled
  2303. }
  2304. func (h *GatewayHandler) maybeLogCompatibilityFallbackMetrics(reqLog *zap.Logger) {
  2305. if reqLog == nil {
  2306. return
  2307. }
  2308. if gatewayCompatibilityMetricsLogCounter.Add(1)%gatewayCompatibilityMetricsLogInterval != 0 {
  2309. return
  2310. }
  2311. metrics := service.SnapshotOpenAICompatibilityFallbackMetrics()
  2312. reqLog.Info("gateway.compatibility_fallback_metrics",
  2313. zap.Int64("session_hash_legacy_read_fallback_total", metrics.SessionHashLegacyReadFallbackTotal),
  2314. zap.Int64("session_hash_legacy_read_fallback_hit", metrics.SessionHashLegacyReadFallbackHit),
  2315. zap.Int64("session_hash_legacy_dual_write_total", metrics.SessionHashLegacyDualWriteTotal),
  2316. zap.Float64("session_hash_legacy_read_hit_rate", metrics.SessionHashLegacyReadHitRate),
  2317. zap.Int64("metadata_legacy_fallback_total", metrics.MetadataLegacyFallbackTotal),
  2318. )
  2319. }
  2320. func (h *GatewayHandler) submitUsageRecordTask(parent context.Context, task service.UsageRecordTask) {
  2321. if task == nil {
  2322. return
  2323. }
  2324. task = wrapUsageRecordTaskContext(parent, task)
  2325. if h.usageRecordWorkerPool != nil {
  2326. if mode := h.usageRecordWorkerPool.Submit(task); mode != service.UsageRecordSubmitModeDroppedStopped {
  2327. return
  2328. }
  2329. // 池已停止(进程关停窗口):计费任务不能静默丢失,降级为内联同步执行。
  2330. // 显式配置的 drop/sample 溢出丢弃仍按配置语义保留。
  2331. logger.L().With(
  2332. zap.String("component", "handler.gateway.messages"),
  2333. ).Warn("gateway.usage_record_task_stopped_sync_fallback")
  2334. }
  2335. // 回退路径:worker 池未注入或已停止时同步执行,避免退回到无界 goroutine 模式。
  2336. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  2337. defer cancel()
  2338. defer func() {
  2339. if recovered := recover(); recovered != nil {
  2340. logger.L().With(
  2341. zap.String("component", "handler.gateway.messages"),
  2342. zap.Any("panic", recovered),
  2343. ).Error("gateway.usage_record_task_panic_recovered")
  2344. }
  2345. }()
  2346. task(ctx)
  2347. }
  2348. // submitMandatoryUsageRecordTask never silently drops billing work on pool overflow.
  2349. func (h *GatewayHandler) submitMandatoryUsageRecordTask(parent context.Context, task service.UsageRecordTask) {
  2350. if task == nil {
  2351. return
  2352. }
  2353. task = wrapUsageRecordTaskContext(parent, task)
  2354. if h.usageRecordWorkerPool != nil {
  2355. if mode := h.usageRecordWorkerPool.Submit(task); !mode.Dropped() {
  2356. return
  2357. }
  2358. logger.L().With(
  2359. zap.String("component", "handler.gateway.usage"),
  2360. ).Warn("gateway.usage_record_task_mandatory_sync_fallback")
  2361. }
  2362. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  2363. defer cancel()
  2364. defer func() {
  2365. if recovered := recover(); recovered != nil {
  2366. logger.L().With(
  2367. zap.String("component", "handler.gateway.usage"),
  2368. zap.Any("panic", recovered),
  2369. ).Error("gateway.usage_record_task_panic_recovered")
  2370. }
  2371. }()
  2372. task(ctx)
  2373. }
  2374. // getUserMsgQueueMode 获取当前请求的 UMQ 模式
  2375. // 返回 "serialize" | "throttle" | ""
  2376. func (h *GatewayHandler) getUserMsgQueueMode(account *service.Account, parsed *service.ParsedRequest) string {
  2377. if h.userMsgQueueHelper == nil {
  2378. return ""
  2379. }
  2380. // 仅适用于 Anthropic OAuth/SetupToken 账号
  2381. if !account.IsAnthropicOAuthOrSetupToken() {
  2382. return ""
  2383. }
  2384. if !service.IsRealUserMessage(parsed) {
  2385. return ""
  2386. }
  2387. // 账号级模式优先,fallback 到全局配置
  2388. mode := account.GetUserMsgQueueMode()
  2389. if mode == "" {
  2390. mode = h.cfg.Gateway.UserMessageQueue.GetEffectiveMode()
  2391. }
  2392. return mode
  2393. }