openai_gateway_handler.go 165 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295
  1. package handler
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "encoding/xml"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "net/http"
  11. "runtime/debug"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. "sync/atomic"
  16. "time"
  17. "github.com/Wei-Shaw/sub2api/internal/config"
  18. "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
  19. "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
  20. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  21. "github.com/Wei-Shaw/sub2api/internal/pkg/requestmodel"
  22. "github.com/Wei-Shaw/sub2api/internal/securityaudit"
  23. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  24. "github.com/Wei-Shaw/sub2api/internal/service"
  25. coderws "github.com/coder/websocket"
  26. "github.com/gin-gonic/gin"
  27. "github.com/google/uuid"
  28. "github.com/tidwall/gjson"
  29. "go.uber.org/zap"
  30. )
  31. // OpenAIGatewayHandler handles OpenAI API gateway requests
  32. type OpenAIGatewayHandler struct {
  33. gatewayService *service.OpenAIGatewayService
  34. billingCacheService *service.BillingCacheService
  35. apiKeyService *service.APIKeyService
  36. usageRecordWorkerPool *service.UsageRecordWorkerPool
  37. errorPassthroughService *service.ErrorPassthroughService
  38. contentModerationService *service.ContentModerationService
  39. securityPolicyService *service.SecurityPolicyService
  40. securityAuditCoordinator *securityaudit.Coordinator
  41. grokMediaEligibilityProber grokMediaEligibilityProber
  42. opsService *service.OpsService
  43. concurrencyHelper *ConcurrencyHelper
  44. imageLimiter *imageConcurrencyLimiter
  45. maxAccountSwitches int
  46. cfg *config.Config
  47. }
  48. type openAIWSTurnChannelMappingSnapshot struct {
  49. turn int
  50. mapping service.ChannelMappingResult
  51. }
  52. func advanceOpenAIWSCyberBlockState(blocked, pending, marked bool, turnErr error) (bool, bool) {
  53. var failoverErr *service.UpstreamFailoverError
  54. isFailover := errors.As(turnErr, &failoverErr)
  55. if marked {
  56. if isFailover {
  57. return false, true
  58. }
  59. return true, false
  60. }
  61. if pending && !isFailover {
  62. return true, false
  63. }
  64. return blocked, pending
  65. }
  66. var errOpenAIWSUnsupportedModelSwitch = errors.New("selected account does not support websocket model switch")
  67. func newOpenAIWSUnsupportedModelSwitchError(model string) error {
  68. cause := fmt.Errorf("%w: model %q", errOpenAIWSUnsupportedModelSwitch, strings.TrimSpace(model))
  69. return service.NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "model switch requires reconnect", cause)
  70. }
  71. func shouldReportOpenAIWSProxyAccountFailure(err error) bool {
  72. return err != nil && !errors.Is(err, errOpenAIWSUnsupportedModelSwitch) && !service.IsOpenAIWSSessionPreemptedError(err)
  73. }
  74. // openAIWSIngressEndedByClient reports whether a finished ingress WebSocket turn
  75. // ended the way a healthy client ends one, rather than through an upstream or
  76. // account fault.
  77. //
  78. // Three error shapes describe that same benign outcome and only the first was
  79. // recognised:
  80. //
  81. // - *service.OpenAIWSClientCloseError carrying 1000 — the gateway closing the
  82. // socket on its own terms, e.g. the inter-turn idle timeout.
  83. // - a bare coderws.CloseError{Code: 1000} — what coder/websocket returns when
  84. // the client closes cleanly. ReadOpenAIWSClientMessage hands conn.Read's
  85. // error back verbatim, so nothing ever wraps it into the type above and an
  86. // errors.As against that type cannot see it.
  87. // - context.Canceled — the client went away mid-turn. That path closes with
  88. // StatusGoingAway (1001) and carries the cancellation as its cause, so a
  89. // check for 1000 alone never matched it either.
  90. //
  91. // The last two fell through to shouldReportOpenAIWSProxyAccountFailure, which
  92. // filters only model-switch and session-preemption errors. Everything else
  93. // reaches ObserveOpenAIAPIKeyHealthFailure and scheduler.ReportResult(false), so
  94. // a client that merely disconnected counted against the upstream account's
  95. // health and could trip it out of scheduling.
  96. //
  97. // failoverClientGone already states the rule this restores for the HTTP failover
  98. // path — a cancelled client context "被误报成账号耗尽" is a bug, not a signal —
  99. // and summarizeWSCloseErrorForLog already reads the close code the correct way,
  100. // which is why the resulting WARN printed close_status=1000(StatusNormalClosure)
  101. // for an error that was, in the same breath, being charged to the account.
  102. //
  103. // Deliberately narrow. StatusGoingAway is not matched on its own: the gateway
  104. // emits 1001 when it tears a session down for its own reasons too, and the
  105. // client-cancellation case is already covered by context.Canceled.
  106. // context.DeadlineExceeded is left out as well — the idle-timeout path wraps it
  107. // in a 1000 close error and stays benign through the first check, while any
  108. // other deadline is a genuine stall worth reporting.
  109. func openAIWSIngressEndedByClient(err error) bool {
  110. if err == nil {
  111. return true
  112. }
  113. var closeErr *service.OpenAIWSClientCloseError
  114. if errors.As(err, &closeErr) && closeErr.StatusCode() == coderws.StatusNormalClosure {
  115. return true
  116. }
  117. if coderws.CloseStatus(err) == coderws.StatusNormalClosure {
  118. return true
  119. }
  120. return errors.Is(err, context.Canceled)
  121. }
  122. func openAIWSTurnBillingModel(result *service.OpenAIForwardResult, mapping service.ChannelMappingResult, requestedModel, upstreamModel string) string {
  123. billingModel := ""
  124. if result != nil {
  125. billingModel = strings.TrimSpace(result.BillingModel)
  126. }
  127. if billingModel == "" {
  128. billingModel = strings.TrimSpace(upstreamModel)
  129. }
  130. if billingModel == "" {
  131. billingModel = strings.TrimSpace(requestedModel)
  132. }
  133. requestedModel = strings.TrimSpace(requestedModel)
  134. switch mapping.BillingModelSource {
  135. case service.BillingModelSourceRequested:
  136. if requestedModel != "" {
  137. billingModel = requestedModel
  138. }
  139. case service.BillingModelSourceChannelMapped:
  140. mappedModel := strings.TrimSpace(mapping.MappedModel)
  141. if mappedModel != "" && mappedModel != requestedModel {
  142. billingModel = mappedModel
  143. }
  144. }
  145. return billingModel
  146. }
  147. // openAIChannelForwardModel returns the model used for upstream capability
  148. // routing. The client-requested model remains separate for logs, billing, and
  149. // responses.
  150. func openAIChannelForwardModel(mapping service.ChannelMappingResult, requestedModel string) string {
  151. if mapping.Mapped {
  152. if mappedModel := strings.TrimSpace(mapping.MappedModel); mappedModel != "" {
  153. return mappedModel
  154. }
  155. }
  156. return requestedModel
  157. }
  158. type grokMediaEligibilityProber interface {
  159. ProbeMediaEligibility(ctx context.Context, accountID int64) (bool, string, error)
  160. }
  161. const maxOpenAIFirstOutputTimeoutSwitches = 1
  162. func openAIForwardSucceededForScheduling(result *service.OpenAIForwardResult) bool {
  163. return result.SucceededForScheduling()
  164. }
  165. func openAIAccountScheduleModel(c *gin.Context, account *service.Account, forwardModel string, requireCompact bool, result *service.OpenAIForwardResult) string {
  166. if result != nil {
  167. if actual := strings.TrimSpace(result.UpstreamModel); actual != "" {
  168. return actual
  169. }
  170. }
  171. if c != nil {
  172. if value, ok := c.Get(service.OpsUpstreamModelKey); ok {
  173. if actual, ok := value.(string); ok && strings.TrimSpace(actual) != "" {
  174. return strings.TrimSpace(actual)
  175. }
  176. }
  177. }
  178. return service.ResolveOpenAIAccountUpstreamModelForRequest(account, forwardModel, requireCompact)
  179. }
  180. func resolveOpenAIMessagesDispatchMappedModel(c *gin.Context, apiKey *service.APIKey, requestedModel string) string {
  181. if apiKey == nil || apiKey.Group == nil {
  182. return ""
  183. }
  184. // composite 解析到 grok/CN 目标时调度级映射不适用(Group 级映射的 gpt-5.x
  185. // 默认值是 openai 专属,发给这些上游必错),模型改写交给账号级 model_mapping。
  186. if apiKey.Group.Platform == service.PlatformComposite && c != nil && c.Request != nil {
  187. if platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok &&
  188. (platform == service.PlatformGrok || service.IsCNProvider(platform)) {
  189. return ""
  190. }
  191. }
  192. return strings.TrimSpace(apiKey.Group.ResolveMessagesDispatchModel(requestedModel))
  193. }
  194. type openAIModelBodyReplaceFunc func([]byte, string) []byte
  195. func openAIModelMappedBody(body []byte, mapped bool, mappedModel string, replace openAIModelBodyReplaceFunc) []byte {
  196. if !mapped || replace == nil {
  197. return body
  198. }
  199. return replace(body, mappedModel)
  200. }
  201. func seedOpenAIForwardImageIntentHint(c *gin.Context, channelMapped bool, imageIntent bool) {
  202. if channelMapped {
  203. // 渠道映射改变了规范请求,保持 unknown,由 Forward 按映射后的 model/body 初始化。
  204. return
  205. }
  206. service.SetOpenAIImageIntentHint(c, imageIntent)
  207. }
  208. func newOpenAIModelMappedBodyCache(body []byte, replace openAIModelBodyReplaceFunc) func(bool, string) []byte {
  209. replacedBodies := make(map[string][]byte)
  210. return func(mapped bool, mappedModel string) []byte {
  211. if !mapped {
  212. return body
  213. }
  214. if cachedBody, ok := replacedBodies[mappedModel]; ok {
  215. return cachedBody
  216. }
  217. replacedBody := openAIModelMappedBody(body, true, mappedModel, replace)
  218. replacedBodies[mappedModel] = replacedBody
  219. return replacedBody
  220. }
  221. }
  222. func usageRecordContext(parent context.Context, base context.Context) context.Context {
  223. if base == nil {
  224. base = context.Background()
  225. }
  226. if parent == nil {
  227. return base
  228. }
  229. if clientRequestID, _ := parent.Value(ctxkey.ClientRequestID).(string); strings.TrimSpace(clientRequestID) != "" {
  230. base = context.WithValue(base, ctxkey.ClientRequestID, strings.TrimSpace(clientRequestID))
  231. }
  232. if requestID, _ := parent.Value(ctxkey.RequestID).(string); strings.TrimSpace(requestID) != "" {
  233. base = context.WithValue(base, ctxkey.RequestID, strings.TrimSpace(requestID))
  234. }
  235. return base
  236. }
  237. func wrapUsageRecordTaskContext(parent context.Context, task service.UsageRecordTask) service.UsageRecordTask {
  238. if task == nil {
  239. return nil
  240. }
  241. return func(ctx context.Context) {
  242. task(usageRecordContext(parent, ctx))
  243. }
  244. }
  245. func openAICompatibleRequestPlatform(ctx context.Context, apiKey *service.APIKey) string {
  246. if platform, ok := service.ResolvedTargetPlatformFromContext(ctx); ok {
  247. // 保留 grok 与国产供应商原值,其他归一为 openai(与调度器精确匹配语义一致)。
  248. return service.NormalizeOpenAICompatiblePlatform(platform)
  249. }
  250. if apiKey != nil && apiKey.Group != nil {
  251. return service.NormalizeOpenAICompatiblePlatform(apiKey.Group.Platform)
  252. }
  253. return service.PlatformOpenAI
  254. }
  255. func openAIResponsesRequiredCapability(imageIntent bool, platform string) service.OpenAIEndpointCapability {
  256. if imageIntent && platform == service.PlatformOpenAI {
  257. return service.OpenAIEndpointCapabilityResponses
  258. }
  259. return service.OpenAIEndpointCapabilityChatCompletions
  260. }
  261. // openAIResponsesRequiredCapabilityForRequest returns the endpoint capability
  262. // required by an image or Responses request. needsResponses includes both the
  263. // legacy /responses/compact endpoint and native remote compaction v2.
  264. func openAIResponsesRequiredCapabilityForRequest(imageIntent bool, needsResponses bool, platform string) service.OpenAIEndpointCapability {
  265. if needsResponses && platform == service.PlatformOpenAI {
  266. return service.OpenAIEndpointCapabilityResponses
  267. }
  268. return openAIResponsesRequiredCapability(imageIntent, platform)
  269. }
  270. func allowOpenAICompatibleMessagesDispatch(c *gin.Context, apiKey *service.APIKey) bool {
  271. if apiKey == nil || apiKey.Group == nil {
  272. return true
  273. }
  274. if apiKey.Group.Platform == service.PlatformGrok {
  275. return true
  276. }
  277. // 国产供应商分组与 grok 同语义:/v1/messages 就是其主要服务形态(anthropic
  278. // 协议账号原生直通 Claude Code),无需 allow_messages_dispatch 开关授权——
  279. // 该开关对非 openai/composite 平台恒被 sanitizeGroupMessagesDispatchFields 置 false,
  280. // 若不豁免,CN 分组将永远 403。
  281. if service.IsCNProvider(apiKey.Group.Platform) {
  282. return true
  283. }
  284. // composite 分组解析到 grok/CN 目标时与对应独立分组同语义豁免;
  285. // 解析到 openai 目标则受 composite 分组自身的可配置开关控制。
  286. if apiKey.Group.Platform == service.PlatformComposite && c != nil && c.Request != nil {
  287. if platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok &&
  288. (platform == service.PlatformGrok || service.IsCNProvider(platform)) {
  289. return true
  290. }
  291. }
  292. return apiKey.Group.AllowMessagesDispatch
  293. }
  294. func openAICompatibleTextTargetAllowed(c *gin.Context, apiKey *service.APIKey, model string) bool {
  295. return compositeTargetPlatformAllowed(c, apiKey, model,
  296. service.PlatformOpenAI, service.PlatformGrok,
  297. service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformMiniMax)
  298. }
  299. // isResponsesWebSocketCompositePlatform 限定 composite 分组在 Responses WebSocket
  300. // 上可服务的目标平台。CN 供应商(kimi/zhipu/deepseek)刻意排除:其账号无法通过
  301. // WSv2 ingress 的 transport 过滤,且 WS HTTP 桥没有面向 CN 的 Responses 转换,
  302. // 放行只会把明确的策略拒绝变成误导性的 "no available account"。
  303. func isResponsesWebSocketCompositePlatform(platform string) bool {
  304. switch platform {
  305. case service.PlatformOpenAI, service.PlatformGrok:
  306. return true
  307. default:
  308. return false
  309. }
  310. }
  311. // NewOpenAIGatewayHandler creates a new OpenAIGatewayHandler
  312. func NewOpenAIGatewayHandler(
  313. gatewayService *service.OpenAIGatewayService,
  314. concurrencyService *service.ConcurrencyService,
  315. billingCacheService *service.BillingCacheService,
  316. apiKeyService *service.APIKeyService,
  317. usageRecordWorkerPool *service.UsageRecordWorkerPool,
  318. errorPassthroughService *service.ErrorPassthroughService,
  319. contentModerationService *service.ContentModerationService,
  320. opsService *service.OpsService,
  321. cfg *config.Config,
  322. ) *OpenAIGatewayHandler {
  323. pingInterval := time.Duration(0)
  324. maxAccountSwitches := 3
  325. if cfg != nil {
  326. pingInterval = time.Duration(cfg.Concurrency.PingInterval) * time.Second
  327. if cfg.Gateway.MaxAccountSwitches > 0 {
  328. maxAccountSwitches = cfg.Gateway.MaxAccountSwitches
  329. }
  330. }
  331. return &OpenAIGatewayHandler{
  332. gatewayService: gatewayService,
  333. billingCacheService: billingCacheService,
  334. apiKeyService: apiKeyService,
  335. usageRecordWorkerPool: usageRecordWorkerPool,
  336. errorPassthroughService: errorPassthroughService,
  337. contentModerationService: contentModerationService,
  338. opsService: opsService,
  339. concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatComment, pingInterval),
  340. imageLimiter: &imageConcurrencyLimiter{},
  341. maxAccountSwitches: maxAccountSwitches,
  342. cfg: cfg,
  343. }
  344. }
  345. // Responses handles OpenAI Responses API endpoint
  346. // POST /openai/v1/responses
  347. func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
  348. // 局部兜底:确保该 handler 内部任何 panic 都不会击穿到进程级。
  349. streamStarted := false
  350. defer h.recoverResponsesPanic(c, &streamStarted)
  351. compactStartedAt := time.Now()
  352. defer h.logOpenAIRemoteCompactOutcome(c, compactStartedAt)
  353. setOpenAIClientTransportHTTP(c)
  354. requestStart := time.Now()
  355. // Get apiKey and user from context (set by ApiKeyAuth middleware)
  356. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  357. if !ok {
  358. h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  359. return
  360. }
  361. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  362. if !ok {
  363. h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  364. return
  365. }
  366. reqLog := requestLogger(
  367. c,
  368. "handler.openai_gateway.responses",
  369. zap.Int64("user_id", subject.UserID),
  370. zap.Int64("api_key_id", apiKey.ID),
  371. zap.Any("group_id", apiKey.GroupID),
  372. )
  373. if !h.ensureResponsesDependencies(c, reqLog) {
  374. return
  375. }
  376. // Read request body
  377. body, err := readLenientJSONRequestBodyWithPrealloc(c.Request, h.cfg)
  378. if err != nil {
  379. if maxErr, ok := extractMaxBytesError(err); ok {
  380. h.errorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
  381. return
  382. }
  383. logRequestBodyReadFailure(reqLog, c.Request, err)
  384. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
  385. return
  386. }
  387. if len(body) == 0 {
  388. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
  389. return
  390. }
  391. setOpsRequestContext(c, "", false)
  392. sessionHashBody := body
  393. body, ok = h.normalizeOpenAIResponsesCompactRequest(c, reqLog, body)
  394. if !ok {
  395. return
  396. }
  397. legacyCompact := service.IsOpenAIResponsesCompactPath(c)
  398. nativeV2 := isBareOpenAIResponsesPath(c) && isOpenAIRemoteCompactionV2Request(body)
  399. if nativeV2 {
  400. // 原生 v2 压缩出站前补注 x-codex-beta-features: remote_compaction_v2,
  401. // 与真实 Codex 线型一致(网关链剥头后本级负责恢复,#5586)。
  402. service.MarkOpenAINativeCompactionV2(c)
  403. }
  404. // body-signal compact:上游 unary 等待期间向下游发 SSE 注释行心跳,防止
  405. // 反向代理空闲超时掐断长压缩连接(#3887)。首拍延迟一个心跳间隔,快速
  406. // 失败仍走 JSON+状态码链路;未标记客户端流式或间隔为 0 时是 no-op。
  407. stopCompactKeepalive := service.StartOpenAICompactSSEKeepalive(c, h.openAICompactKeepaliveInterval())
  408. defer stopCompactKeepalive()
  409. // 校验请求体 JSON 合法性
  410. if !gjson.ValidBytes(body) {
  411. logRequestBodyParseFailure(reqLog, body, nil)
  412. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  413. return
  414. }
  415. // 使用 gjson 只读提取字段做校验,避免完整 Unmarshal
  416. modelResult := gjson.GetBytes(body, "model")
  417. if !modelResult.Exists() || modelResult.Type != gjson.String || modelResult.String() == "" {
  418. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  419. return
  420. }
  421. reqModel := modelResult.String()
  422. ensureCompositeTargetPlatform(c, apiKey, reqModel)
  423. if !openAICompatibleTextTargetAllowed(c, apiKey, reqModel) {
  424. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
  425. return
  426. }
  427. if cappedBody, changed, err := applyOpenAIReasoningEffortPolicyForRequest(c, apiKey, body); err != nil {
  428. respondOpenAIReasoningEffortPolicyError(c, err, h.errorResponse)
  429. return
  430. } else if changed {
  431. body = cappedBody
  432. }
  433. if normalizedBody, changed := normalizeCodexAutomationBootstrap(body); changed {
  434. body = normalizedBody
  435. reqLog.Info("openai.codex_automation_bootstrap_normalized",
  436. zap.String("normalization", "call_output_to_user_message"),
  437. )
  438. }
  439. if normalizedBody, changed := normalizeCodexDelegationBootstrap(body); changed {
  440. body = normalizedBody
  441. reqLog.Info("openai.codex_delegation_bootstrap_normalized",
  442. zap.String("normalization", "call_output_to_user_message"),
  443. )
  444. }
  445. reqStream, ok := parseOpenAICompatibleStream(body)
  446. if !ok {
  447. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", invalidStreamFieldTypeMessage)
  448. return
  449. }
  450. if _, err := service.ValidateOpenAIServiceTierField(body); err != nil {
  451. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", err.Error())
  452. return
  453. }
  454. reqLog = reqLog.With(zap.String("model", reqModel), zap.Bool("stream", reqStream))
  455. previousResponseID := strings.TrimSpace(gjson.GetBytes(body, "previous_response_id").String())
  456. if previousResponseID != "" {
  457. previousResponseIDKind := service.ClassifyOpenAIPreviousResponseIDKind(previousResponseID)
  458. reqLog = reqLog.With(
  459. zap.Bool("has_previous_response_id", true),
  460. zap.String("previous_response_id_kind", previousResponseIDKind),
  461. zap.Int("previous_response_id_len", len(previousResponseID)),
  462. )
  463. if previousResponseIDKind == service.OpenAIPreviousResponseIDKindMessageID {
  464. reqLog.Warn("openai.request_validation_failed",
  465. zap.String("reason", "previous_response_id_looks_like_message_id"),
  466. )
  467. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "previous_response_id must be a response.id (resp_*), not a message id")
  468. return
  469. }
  470. groupID := int64(0)
  471. if apiKey.GroupID != nil {
  472. groupID = *apiKey.GroupID
  473. }
  474. owned, ownershipErr := h.gatewayService.ValidateOpenAIHTTPResponseOwner(
  475. c.Request.Context(),
  476. groupID,
  477. previousResponseID,
  478. subject.UserID,
  479. apiKey.ID,
  480. )
  481. if ownershipErr != nil {
  482. reqLog.Warn("openai.previous_response_owner_lookup_failed", zap.Error(ownershipErr))
  483. }
  484. if !owned {
  485. reqLog.Warn("openai.request_validation_failed", zap.String("reason", "previous_response_owner_mismatch"))
  486. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "previous_response_id is not available for this user")
  487. return
  488. }
  489. }
  490. service.SetOpenAIHTTPResponseOwner(c, subject.UserID, apiKey.ID)
  491. setOpsRequestContext(c, reqModel, reqStream)
  492. setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(reqStream, false)))
  493. if decision := h.checkSecurityAudit(c, reqLog, apiKey, subject, service.ContentModerationProtocolOpenAIResponses, reqModel, body); decision != nil && !decision.AllowNextStage {
  494. h.openAISecurityAuditError(c, decision)
  495. return
  496. }
  497. // 使用 IsExplicitImageGenerationIntent 排除被动 image_gen namespace 声明。
  498. // Codex 在所有请求中被动声明 image_gen namespace,宽泛检测会导致禁了生图的
  499. // 分组中所有 Codex 请求被 403(#4447),并误占生图并发槽位。
  500. imageIntent := service.IsExplicitImageGenerationIntent("/v1/responses", reqModel, body)
  501. if imageIntent && !service.GroupAllowsImageGeneration(apiKey.Group) {
  502. h.errorResponse(c, http.StatusForbidden, "permission_error", service.ImageGenerationPermissionMessage())
  503. return
  504. }
  505. var imageReleaseFunc func()
  506. if imageIntent {
  507. var imageAcquired bool
  508. imageReleaseFunc, imageAcquired = h.acquireImageGenerationSlot(c, streamStarted)
  509. if !imageAcquired {
  510. return
  511. }
  512. if imageReleaseFunc != nil {
  513. defer imageReleaseFunc()
  514. }
  515. }
  516. // 解析渠道级模型映射
  517. channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel)
  518. forwardBody := openAIModelMappedBody(body, channelMapping.Mapped, channelMapping.MappedModel, h.gatewayService.ReplaceModelInBody)
  519. seedOpenAIForwardImageIntentHint(c, channelMapping.Mapped, imageIntent)
  520. forwardModel := openAIChannelForwardModel(channelMapping, reqModel)
  521. c.Request = c.Request.WithContext(service.WithOpenAIForwardModel(
  522. c.Request.Context(),
  523. forwardModel,
  524. legacyCompact,
  525. ))
  526. // 提前校验 function_call_output 是否具备可关联上下文,避免上游 400。
  527. if !h.validateFunctionCallOutputRequest(c, body, reqLog) {
  528. return
  529. }
  530. // 绑定错误透传服务,允许 service 层在非 failover 错误场景复用规则。
  531. if h.errorPassthroughService != nil {
  532. service.BindErrorPassthroughService(c, h.errorPassthroughService)
  533. }
  534. // Get subscription info (may be nil)
  535. subscription, _ := middleware2.GetSubscriptionFromContext(c)
  536. requestPlatform := openAICompatibleRequestPlatform(c.Request.Context(), apiKey)
  537. service.SetOpsLatencyMs(c, service.OpsAuthLatencyMsKey, time.Since(requestStart).Milliseconds())
  538. routingStart := time.Now()
  539. userReleaseFunc, acquired := h.acquireResponsesUserSlot(c, subject.UserID, subject.Concurrency, reqStream, &streamStarted, reqLog)
  540. if !acquired {
  541. return
  542. }
  543. // 确保请求取消时也会释放槽位,避免长连接被动中断造成泄漏
  544. if userReleaseFunc != nil {
  545. defer userReleaseFunc()
  546. }
  547. // 2. Re-check billing eligibility after wait
  548. if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription, service.QuotaPlatform(c.Request.Context(), apiKey)); err != nil {
  549. reqLog.Info("openai.billing_eligibility_check_failed", zap.Error(err))
  550. status, code, message, retryAfter := billingErrorDetails(err)
  551. if retryAfter > 0 {
  552. c.Header("Retry-After", strconv.Itoa(retryAfter))
  553. }
  554. h.handleStreamingAwareError(c, status, code, message, streamStarted)
  555. return
  556. }
  557. // Generate session hash (header first; fallback to prompt_cache_key)
  558. sessionHash := h.gatewayService.GenerateSessionHash(c, sessionHashBody)
  559. if h.rejectIfCyberSessionBlocked(c, apiKey, sessionHashBody, reqModel, cyberBlockFormatResponses) {
  560. return
  561. }
  562. c.Request = c.Request.WithContext(service.WithOpenAIGuardianParentAffinity(
  563. c.Request.Context(), c, sessionHashBody, reqModel,
  564. ))
  565. requireCompact := legacyCompact
  566. maxAccountSwitches := h.maxAccountSwitches
  567. switchCount := 0
  568. firstOutputTimeoutSwitchCount := 0
  569. profitVetoCount := 0
  570. failedAccountIDs := make(map[int64]struct{})
  571. sameAccountRetryCount := make(map[int64]int)
  572. var lastFailoverErr *service.UpstreamFailoverError
  573. var oauth429FailoverState service.OpenAIOAuth429FailoverState
  574. var passthroughFailoverState openAIPassthroughFailoverState
  575. // 生图意图的 /v1/responses 请求必须调度到确实支持 Responses API 的账号,否则
  576. // 会在 forward 阶段被静默降级为无法生图的 Chat Completions 直转(#4417)。
  577. // 仅对 OpenAI 平台生效:Grok 生图走独立的 forwardGrokResponses 路径,不应被过滤。
  578. // 复用前置权限与并发阶段在未修改 body 上确认的显式生图意图,避免大 tools 请求重复扫描。
  579. // 该判断已排除 Codex 被动 image_gen namespace,避免 CC-only 账号被误过滤(#4476)。
  580. needsResponses := nativeV2 || legacyCompact
  581. requiredCapability := openAIResponsesRequiredCapabilityForRequest(imageIntent, needsResponses, requestPlatform)
  582. // 分组利润控制:请求级装配定价上下文——pricingAt 固定本请求的
  583. // D 与计费高峰因子,选号、槽位终检与全部 failover 重入共用同一门与阈值。
  584. // 生图意图只影响能力路由与图片计费,不关门:混合 /v1/responses 请求的
  585. // token 计费部分仍受利润门保护,独立图片/视频端点才在门外。
  586. pricingCtx, pricingAt := h.gatewayService.WithOpenAIRequestPricingContext(c.Request.Context(), apiKey.GroupID)
  587. c.Request = c.Request.WithContext(pricingCtx)
  588. for {
  589. // Streaming Forward intentionally detaches the upstream request so usage can
  590. // be drained after a disconnect. Re-check the client context before every
  591. // account attempt so a canceled request never starts a failover replay.
  592. if !openAIRequestAllowsFailoverReplay(c) {
  593. return
  594. }
  595. // Select account supporting the requested model
  596. reqLog.Debug("openai.account_selecting", zap.Int("excluded_account_count", len(failedAccountIDs)))
  597. selection, scheduleDecision, err := h.gatewayService.SelectAccountWithSchedulerForCapability(
  598. c.Request.Context(),
  599. apiKey.GroupID,
  600. previousResponseID,
  601. sessionHash,
  602. forwardModel,
  603. failedAccountIDs,
  604. service.OpenAIUpstreamTransportAny,
  605. requiredCapability,
  606. requireCompact,
  607. false,
  608. !imageIntent,
  609. requestPlatform,
  610. )
  611. if err != nil {
  612. if failoverClientGone(c) {
  613. reqLog.Info("openai.account_select_aborted_client_disconnected", zap.Error(err))
  614. return
  615. }
  616. reqLog.Warn("openai.account_select_failed",
  617. zap.Error(openAICompatibleSelectionErrorForLog(err, requestPlatform)),
  618. zap.Int("excluded_account_count", len(failedAccountIDs)),
  619. )
  620. if len(failedAccountIDs) == 0 {
  621. if legacyCompact && errors.Is(err, service.ErrNoAvailableCompactAccounts) {
  622. markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
  623. h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "compact_not_supported", "No available accounts support /responses/compact", streamStarted)
  624. return
  625. }
  626. cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, reqModel, reqModel, requestPlatform)
  627. cls = classifySelectionFailureError(err, cls)
  628. if !cls.ModelNotFound {
  629. markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
  630. }
  631. h.handleStreamingAwareError(c, cls.Status, cls.ErrType, cls.Message, streamStarted)
  632. return
  633. }
  634. if lastFailoverErr != nil {
  635. h.handleFailoverExhausted(c, lastFailoverErr, streamStarted)
  636. } else {
  637. h.handleFailoverExhaustedSimple(c, 502, streamStarted)
  638. }
  639. return
  640. }
  641. if selection == nil || selection.Account == nil {
  642. cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, reqModel, reqModel, requestPlatform)
  643. if !cls.ModelNotFound {
  644. markOpsRoutingCapacityLimited(c)
  645. }
  646. h.handleStreamingAwareError(c, cls.Status, cls.ErrType, cls.Message, streamStarted)
  647. return
  648. }
  649. if previousResponseID != "" && selection != nil && selection.Account != nil {
  650. reqLog.Debug("openai.account_selected_with_previous_response_id", zap.Int64("account_id", selection.Account.ID))
  651. }
  652. reqLog.Debug("openai.account_schedule_decision",
  653. zap.String("layer", scheduleDecision.Layer),
  654. zap.Bool("sticky_previous_hit", scheduleDecision.StickyPreviousHit),
  655. zap.Bool("sticky_session_hit", scheduleDecision.StickySessionHit),
  656. zap.Int("candidate_count", scheduleDecision.CandidateCount),
  657. zap.Int("top_k", scheduleDecision.TopK),
  658. zap.Int64("latency_ms", scheduleDecision.LatencyMs),
  659. zap.Float64("load_skew", scheduleDecision.LoadSkew),
  660. )
  661. account := selection.Account
  662. if previousResponseID != "" && requestPlatform == service.PlatformOpenAI && !account.IsOpenAIApiKey() {
  663. // The public Responses HTTP API supports previous_response_id on API-key
  664. // accounts. OAuth/SetupToken upstreams do not, so keep searching instead
  665. // of silently deleting continuation state from a mixed account pool.
  666. failedAccountIDs[account.ID] = struct{}{}
  667. if selection.ReleaseFunc != nil {
  668. selection.ReleaseFunc()
  669. selection.ReleaseFunc = nil
  670. }
  671. lastFailoverErr = &service.UpstreamFailoverError{
  672. StatusCode: http.StatusBadRequest,
  673. Stage: service.GatewayFailureStageInference,
  674. Scope: service.GatewayFailureScopeRequest,
  675. Reason: service.OpenAIHTTPContinuationUnsupportedReason,
  676. ClientStatusCode: http.StatusBadRequest,
  677. ClientMessage: "previous_response_id requires an OpenAI API-key account for HTTP requests",
  678. }
  679. reqLog.Debug("openai.account_skipped_http_continuation_unsupported",
  680. zap.Int64("account_id", account.ID),
  681. zap.String("account_type", account.Type),
  682. )
  683. continue
  684. }
  685. sessionHash = ensureOpenAIPoolModeSessionHash(sessionHash, account)
  686. reqLog.Debug("openai.account_selected", zap.Int64("account_id", account.ID), zap.String("account_name", account.Name))
  687. setOpsSelectedAccount(c, account.ID, account.Platform)
  688. accountReleaseFunc, slotResult := h.acquireResponsesAccountSlot(c, apiKey.GroupID, sessionHash, selection, reqStream, &streamStarted, reqLog)
  689. if slotResult == openAISlotAcquireProfitVetoed {
  690. // 利润终检否决:排除该账号重新选号,全池耗尽由下一轮选号报错;
  691. // 否决次数达上限则直接终止,避免排队抢槽后才终检的延迟放大。
  692. if !recordOpenAIProfitVeto(failedAccountIDs, account.ID, &profitVetoCount) {
  693. h.handleOpenAIProfitVetoExhausted(c, streamStarted, reqLog, profitVetoCount)
  694. return
  695. }
  696. continue
  697. }
  698. if slotResult != openAISlotAcquireOK {
  699. return
  700. }
  701. // Forward request
  702. service.SetOpsLatencyMs(c, service.OpsRoutingLatencyMsKey, time.Since(routingStart).Milliseconds())
  703. forwardStart := time.Now()
  704. // 用扣除非语义心跳字节的口径快照:心跳注释不构成语义响应,
  705. // 不能因心跳字节变化而放弃 failover 换号(#3887)。
  706. writerSizeBeforeForward := service.OpenAICompactKeepaliveAdjustedWrittenSize(c)
  707. // 跨 passthrough 边界的 failover:从 Kiro 等透传账号切到 Bedrock 等非透传账号前,
  708. // 从不可变的 canonical forwardBody 派生本次尝试 body 并整块剔除上游私有的加密
  709. // reasoning item(含耦合的 id/summary),避免非透传上游 400 拒绝 Kiro reasoning 形态。
  710. attemptBody := h.deriveOpenAIForwardAttemptBody(reqLog, forwardBody, account, &passthroughFailoverState)
  711. result, err := func() (*service.OpenAIForwardResult, error) {
  712. defer func() {
  713. if accountReleaseFunc != nil {
  714. accountReleaseFunc()
  715. }
  716. }()
  717. return h.gatewayService.Forward(c.Request.Context(), c, account, attemptBody)
  718. }()
  719. var cyberBlockBodyHTTP []byte
  720. if service.GetOpsCyberPolicy(c) != nil {
  721. cyberBlockBodyHTTP = sessionHashBody
  722. }
  723. h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, err != nil, cyberBlockBodyHTTP, clientRequestedUsageFields(c, channelMapping, reqModel, ""), service.HashUsageRequestPayload(body))
  724. forwardDurationMs := time.Since(forwardStart).Milliseconds()
  725. upstreamLatencyMs, _ := getContextInt64(c, service.OpsUpstreamLatencyMsKey)
  726. responseLatencyMs := forwardDurationMs
  727. if upstreamLatencyMs > 0 && forwardDurationMs > upstreamLatencyMs {
  728. responseLatencyMs = forwardDurationMs - upstreamLatencyMs
  729. }
  730. service.SetOpsLatencyMs(c, service.OpsResponseLatencyMsKey, responseLatencyMs)
  731. if err == nil && result != nil && result.FirstTokenMs != nil {
  732. service.SetOpsLatencyMs(c, service.OpsTimeToFirstTokenMsKey, int64(*result.FirstTokenMs))
  733. }
  734. // #5148 对齐:错误返回携带的部分 result(流中断前上游已计量的 usage)照常
  735. // 入账;failover 错误恒定 result=nil,不会重复计费。
  736. submitResponsesUsage := func(res *service.OpenAIForwardResult) {
  737. if res == nil {
  738. return
  739. }
  740. stampOpenAIRequestedReasoningEffort(res, c)
  741. userAgent := c.GetHeader("User-Agent")
  742. clientIP := ip.GetClientIP(c)
  743. requestPayloadHash := service.HashUsageRequestPayload(body)
  744. inboundEndpoint := GetInboundEndpoint(c)
  745. upstreamEndpoint := resolveOpenAIUpstreamEndpoint(c, account, res)
  746. quotaPlatform := service.QuotaPlatform(c.Request.Context(), apiKey)
  747. sessionID := service.ExtractClientSessionID(c)
  748. cyberBlocked := service.GetOpsCyberPolicy(c) != nil
  749. h.submitOpenAIUsageRecordTask(c.Request.Context(), res, func(ctx context.Context) {
  750. if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{
  751. Result: res,
  752. APIKey: apiKey,
  753. User: apiKey.User,
  754. Account: account,
  755. Subscription: subscription,
  756. InboundEndpoint: inboundEndpoint,
  757. UpstreamEndpoint: upstreamEndpoint,
  758. UserAgent: userAgent,
  759. IPAddress: clientIP,
  760. RequestPayloadHash: requestPayloadHash,
  761. APIKeyService: h.apiKeyService,
  762. QuotaPlatform: quotaPlatform,
  763. SessionID: sessionID,
  764. ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, reqModel, res.UpstreamModel),
  765. PricingAt: pricingAt,
  766. CyberBlocked: cyberBlocked,
  767. NativeCompactionV2: nativeV2,
  768. }); err != nil {
  769. logger.L().With(
  770. zap.String("component", "handler.openai_gateway.responses"),
  771. zap.Int64("user_id", subject.UserID),
  772. zap.Int64("api_key_id", apiKey.ID),
  773. zap.Any("group_id", apiKey.GroupID),
  774. zap.String("model", reqModel),
  775. zap.Int64("account_id", account.ID),
  776. ).Error("openai.record_usage_failed", zap.Error(err))
  777. }
  778. })
  779. }
  780. if err != nil {
  781. if result != nil && result.ClientDisconnect {
  782. reqLog.Info("openai.client_disconnected",
  783. zap.Int64("account_id", account.ID),
  784. zap.Error(err),
  785. )
  786. submitResponsesUsage(result)
  787. return
  788. }
  789. if failoverClientGone(c) {
  790. reqLog.Info("openai.client_disconnected",
  791. zap.Int64("account_id", account.ID),
  792. zap.Error(err),
  793. )
  794. submitResponsesUsage(result)
  795. return
  796. }
  797. if result != nil && result.ImageCount > 0 {
  798. reqLog.Warn("openai.forward_partial_error_with_image_result",
  799. zap.Int64("account_id", account.ID),
  800. zap.Int("image_count", result.ImageCount),
  801. zap.Error(err),
  802. )
  803. } else {
  804. var failoverErr *service.UpstreamFailoverError
  805. if errors.As(err, &failoverErr) {
  806. if failoverClientGone(c) {
  807. reqLog.Info("openai.failover_aborted_client_disconnected",
  808. zap.Int64("account_id", account.ID),
  809. zap.Int("upstream_status", failoverErr.StatusCode),
  810. )
  811. return
  812. }
  813. if !openAIForwardMayFailover(c, writerSizeBeforeForward, failoverErr) {
  814. h.gatewayService.ObserveOpenAIAccountHealthFailure(c.Request.Context(), account, err)
  815. h.handleFailoverExhausted(c, failoverErr, true)
  816. return
  817. }
  818. // openAIForwardMayFailover 已确认写出的字节不含语义输出,
  819. // 但重试耗尽时仍须按已提交的 SSE 响应返回流内错误。
  820. if c.Writer.Written() {
  821. streamStarted = true
  822. }
  823. if failoverErr.ShouldReportAccountScheduleFailure() {
  824. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, forwardModel, requireCompact, nil), false, nil, err)
  825. }
  826. if !failoverErr.ShouldRetryNextAccount() {
  827. h.handleFailoverExhausted(c, failoverErr, streamStarted)
  828. return
  829. }
  830. if openAIFirstOutputFailoverExhausted(failoverErr, &firstOutputTimeoutSwitchCount) {
  831. h.handleFailoverExhausted(c, failoverErr, streamStarted)
  832. return
  833. }
  834. // 池模式:同账号重试
  835. if failoverErr.RetryableOnSameAccount {
  836. retryLimit := effectiveSameAccountRetryLimit(failoverErr, account)
  837. if sameAccountRetryAllowed(failoverErr, sameAccountRetryCount[account.ID], retryLimit) {
  838. sameAccountRetryCount[account.ID]++
  839. retryDelay := sameAccountRetryDelayFor(failoverErr, sameAccountRetryCount[account.ID])
  840. reqLog.Warn("openai.pool_mode_same_account_retry",
  841. zap.Int64("account_id", account.ID),
  842. zap.Int("upstream_status", failoverErr.StatusCode),
  843. zap.Int("retry_limit", retryLimit),
  844. zap.Int("retry_count", sameAccountRetryCount[account.ID]),
  845. zap.Duration("retry_delay", retryDelay),
  846. )
  847. select {
  848. case <-c.Request.Context().Done():
  849. return
  850. case <-time.After(retryDelay):
  851. }
  852. continue
  853. }
  854. }
  855. h.gatewayService.RecordOpenAIAccountSwitch()
  856. failedAccountIDs[account.ID] = struct{}{}
  857. lastFailoverErr = failoverErr
  858. if switchCount >= maxAccountSwitches {
  859. h.handleFailoverExhausted(c, failoverErr, streamStarted)
  860. return
  861. }
  862. switchCount++
  863. if h.gatewayService.ShouldStopOpenAIOAuth429Failover(account, failoverErr.StatusCode, switchCount, &oauth429FailoverState) {
  864. h.handleFailoverExhausted(c, failoverErr, streamStarted)
  865. return
  866. }
  867. failoverSwitchFields := []zap.Field{
  868. zap.Int64("account_id", account.ID),
  869. zap.Int("upstream_status", failoverErr.StatusCode),
  870. zap.Int("switch_count", switchCount),
  871. zap.Int("max_switches", maxAccountSwitches),
  872. }
  873. if account.Proxy != nil {
  874. failoverSwitchFields = append(failoverSwitchFields,
  875. zap.Int64("proxy_id", account.Proxy.ID),
  876. zap.String("proxy_name", account.Proxy.Name),
  877. zap.String("proxy_host", account.Proxy.Host),
  878. zap.Int("proxy_port", account.Proxy.Port),
  879. )
  880. } else if account.ProxyID != nil {
  881. failoverSwitchFields = append(failoverSwitchFields, zap.Int64p("proxy_id", account.ProxyID))
  882. }
  883. reqLog.Warn("openai.upstream_failover_switching", failoverSwitchFields...)
  884. continue
  885. }
  886. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, forwardModel, requireCompact, result), false, nil, err)
  887. upstreamErrorAlreadyCommunicated := openAIForwardErrorAlreadyCommunicated(c, writerSizeBeforeForward, err)
  888. wroteFallback := false
  889. if !upstreamErrorAlreadyCommunicated {
  890. wroteFallback = h.ensureForwardErrorResponse(c, streamStarted)
  891. }
  892. fields := []zap.Field{
  893. zap.Int64("account_id", account.ID),
  894. zap.Bool("fallback_error_response_written", wroteFallback),
  895. zap.Bool("upstream_error_response_already_written", upstreamErrorAlreadyCommunicated),
  896. zap.Error(err),
  897. }
  898. submitResponsesUsage(result)
  899. if shouldLogOpenAIForwardFailureAsWarn(c, wroteFallback) {
  900. reqLog.Warn("openai.forward_failed", fields...)
  901. return
  902. }
  903. reqLog.Error("openai.forward_failed", fields...)
  904. return
  905. }
  906. }
  907. if result != nil {
  908. // 排除 spark 影子:其 codex_* 仅由 QueryUsage(/wham/usage bengalfox)更新(外审第7轮 P1)。
  909. if account.Type == service.AccountTypeOAuth && !account.IsShadow() {
  910. h.gatewayService.UpdateCodexUsageSnapshotFromHeaders(c.Request.Context(), account.ID, result.ResponseHeaders)
  911. }
  912. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, forwardModel, requireCompact, result), openAIForwardSucceededForScheduling(result), result.FirstTokenMs)
  913. } else {
  914. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, forwardModel, requireCompact, result), openAIForwardSucceededForScheduling(result), nil)
  915. }
  916. // 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
  917. submitResponsesUsage(result)
  918. reqLog.Debug("openai.request_completed",
  919. zap.Int64("account_id", account.ID),
  920. zap.Int("switch_count", switchCount),
  921. )
  922. return
  923. }
  924. }
  925. func isOpenAILegacyCompactPath(c *gin.Context) bool {
  926. return service.IsOpenAIResponsesCompactPath(c)
  927. }
  928. // isBareOpenAIResponsesPath 仅匹配裸 /responses 端点(无 /compact 等子路径),
  929. // body-signal 提升只允许发生在这里,避免误伤 /responses/{id}/... 形态的请求。
  930. func isBareOpenAIResponsesPath(c *gin.Context) bool {
  931. if c == nil || c.Request == nil || c.Request.URL == nil {
  932. return false
  933. }
  934. normalizedPath := strings.TrimRight(strings.TrimSpace(c.Request.URL.Path), "/")
  935. switch normalizedPath {
  936. case EndpointResponses, "/openai/v1/responses", "/responses", "/backend-api/codex/responses":
  937. return true
  938. default:
  939. return false
  940. }
  941. }
  942. func isOpenAIRemoteCompactionV2Request(body []byte) bool {
  943. stream, valid := parseOpenAICompatibleStream(body)
  944. return valid && stream && service.HasCompactionTriggerInInput(body)
  945. }
  946. // normalizeOpenAIResponsesCompactRequest keeps Codex remote compaction v2 on
  947. // its native streaming /responses wire and preserves the legacy body-signal
  948. // promotion for non-streaming requests.
  949. // 返回归一化后的 body;ok=false 表示错误响应已写出,调用方应直接 return。
  950. func (h *OpenAIGatewayHandler) normalizeOpenAIResponsesCompactRequest(c *gin.Context, reqLog *zap.Logger, body []byte) ([]byte, bool) {
  951. isCompactRequest := isOpenAILegacyCompactPath(c)
  952. if !isCompactRequest && isBareOpenAIResponsesPath(c) && service.HasCompactionTriggerInInput(body) {
  953. if normalized, changed, err := service.NormalizeCompactionTriggerInputOrder(body); err != nil {
  954. reqLog.Warn("codex.remote_compact.trigger_order_normalization_failed", zap.Error(err))
  955. } else if changed {
  956. body = normalized
  957. }
  958. if isOpenAIRemoteCompactionV2Request(body) {
  959. return body, true
  960. }
  961. c.Request.URL.Path = strings.TrimRight(c.Request.URL.Path, "/") + "/compact"
  962. isCompactRequest = true
  963. clientStream := gjson.GetBytes(body, "stream").Bool()
  964. if clientStream {
  965. service.MarkOpenAICompactClientStream(c)
  966. }
  967. reqLog.Info("codex.remote_compact.detected_body_signal", zap.Bool("client_stream", clientStream))
  968. }
  969. if !isCompactRequest {
  970. return body, true
  971. }
  972. if compactSeed := strings.TrimSpace(gjson.GetBytes(body, "prompt_cache_key").String()); compactSeed != "" {
  973. c.Set(service.OpenAICompactSessionSeedKeyForTest(), compactSeed)
  974. }
  975. normalizedCompactBody, normalizedCompact, compactErr := service.NormalizeOpenAICompactRequestBodyForTest(body)
  976. if compactErr != nil {
  977. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to normalize compact request body")
  978. return nil, false
  979. }
  980. if normalizedCompact {
  981. body = normalizedCompactBody
  982. }
  983. return body, true
  984. }
  985. func (h *OpenAIGatewayHandler) logOpenAIRemoteCompactOutcome(c *gin.Context, startedAt time.Time) {
  986. if !isOpenAILegacyCompactPath(c) {
  987. return
  988. }
  989. var (
  990. ctx = context.Background()
  991. path string
  992. status int
  993. )
  994. if c != nil {
  995. if c.Request != nil {
  996. ctx = c.Request.Context()
  997. if c.Request.URL != nil {
  998. path = strings.TrimSpace(c.Request.URL.Path)
  999. }
  1000. }
  1001. if c.Writer != nil {
  1002. status = c.Writer.Status()
  1003. }
  1004. }
  1005. outcome := "failed"
  1006. if status >= 200 && status < 300 {
  1007. outcome = "succeeded"
  1008. }
  1009. // compact 心跳提交后失败的 wire 状态码固化为 200,真实结局以流内错误
  1010. // 标记为准(response.failed 降级路径会 MarkOpsStreamError)。
  1011. if outcome == "succeeded" && c != nil {
  1012. if _, hasStreamErr := service.GetOpsStreamError(c); hasStreamErr {
  1013. outcome = "failed"
  1014. }
  1015. }
  1016. latencyMs := time.Since(startedAt).Milliseconds()
  1017. if latencyMs < 0 {
  1018. latencyMs = 0
  1019. }
  1020. fields := []zap.Field{
  1021. zap.String("component", "handler.openai_gateway.responses"),
  1022. zap.Bool("remote_compact", true),
  1023. zap.String("compact_outcome", outcome),
  1024. zap.Int("status_code", status),
  1025. zap.Int64("latency_ms", latencyMs),
  1026. zap.String("path", path),
  1027. zap.Bool("force_codex_cli", h != nil && h.cfg != nil && h.cfg.Gateway.ForceCodexCLI),
  1028. }
  1029. if c != nil {
  1030. if userAgent := strings.TrimSpace(c.GetHeader("User-Agent")); userAgent != "" {
  1031. fields = append(fields, zap.String("request_user_agent", userAgent))
  1032. }
  1033. if v, ok := c.Get(opsModelKey); ok {
  1034. if model, ok := v.(string); ok && strings.TrimSpace(model) != "" {
  1035. fields = append(fields, zap.String("request_model", strings.TrimSpace(model)))
  1036. }
  1037. }
  1038. if v, ok := c.Get(opsAccountIDKey); ok {
  1039. if accountID, ok := v.(int64); ok && accountID > 0 {
  1040. fields = append(fields, zap.Int64("account_id", accountID))
  1041. }
  1042. }
  1043. if c.Writer != nil {
  1044. if upstreamRequestID := strings.TrimSpace(c.Writer.Header().Get("x-request-id")); upstreamRequestID != "" {
  1045. fields = append(fields, zap.String("upstream_request_id", upstreamRequestID))
  1046. } else if upstreamRequestID := strings.TrimSpace(c.Writer.Header().Get("X-Request-Id")); upstreamRequestID != "" {
  1047. fields = append(fields, zap.String("upstream_request_id", upstreamRequestID))
  1048. }
  1049. }
  1050. }
  1051. log := logger.FromContext(ctx).With(fields...)
  1052. if outcome == "succeeded" {
  1053. log.Info("codex.remote_compact.succeeded")
  1054. return
  1055. }
  1056. log.Warn("codex.remote_compact.failed")
  1057. }
  1058. // Messages handles Anthropic Messages API requests routed to OpenAI platform.
  1059. // POST /v1/messages (when group platform is OpenAI)
  1060. func (h *OpenAIGatewayHandler) Messages(c *gin.Context) {
  1061. streamStarted := false
  1062. defer h.recoverAnthropicMessagesPanic(c, &streamStarted)
  1063. requestStart := time.Now()
  1064. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  1065. if !ok {
  1066. h.anthropicErrorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  1067. return
  1068. }
  1069. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  1070. if !ok {
  1071. h.anthropicErrorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  1072. return
  1073. }
  1074. reqLog := requestLogger(
  1075. c,
  1076. "handler.openai_gateway.messages",
  1077. zap.Int64("user_id", subject.UserID),
  1078. zap.Int64("api_key_id", apiKey.ID),
  1079. zap.Any("group_id", apiKey.GroupID),
  1080. )
  1081. // 检查分组是否允许 /v1/messages 调度
  1082. if !allowOpenAICompatibleMessagesDispatch(c, apiKey) {
  1083. h.anthropicErrorResponse(c, http.StatusForbidden, "permission_error",
  1084. "This group does not allow /v1/messages dispatch")
  1085. return
  1086. }
  1087. if !h.ensureResponsesDependencies(c, reqLog) {
  1088. return
  1089. }
  1090. body, err := readLenientJSONRequestBodyWithPrealloc(c.Request, h.cfg)
  1091. if err != nil {
  1092. if maxErr, ok := extractMaxBytesError(err); ok {
  1093. h.anthropicErrorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
  1094. return
  1095. }
  1096. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
  1097. return
  1098. }
  1099. if len(body) == 0 {
  1100. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
  1101. return
  1102. }
  1103. if !gjson.ValidBytes(body) {
  1104. logRequestBodyParseFailure(reqLog, body, nil)
  1105. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
  1106. return
  1107. }
  1108. modelResult := gjson.GetBytes(body, "model")
  1109. if !modelResult.Exists() || modelResult.Type != gjson.String || modelResult.String() == "" {
  1110. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  1111. return
  1112. }
  1113. reqModel := modelResult.String()
  1114. ensureCompositeTargetPlatform(c, apiKey, reqModel)
  1115. if !openAICompatibleTextTargetAllowed(c, apiKey, reqModel) {
  1116. h.anthropicErrorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
  1117. return
  1118. }
  1119. bindOpenAIReasoningEffortPolicyForMessagesRequest(c, apiKey, body)
  1120. routingModel := service.NormalizeOpenAICompatRequestedModel(reqModel)
  1121. preferredMappedModel := resolveOpenAIMessagesDispatchMappedModel(c, apiKey, reqModel)
  1122. reqStream := gjson.GetBytes(body, "stream").Bool()
  1123. reqLog = reqLog.With(zap.String("model", reqModel), zap.Bool("stream", reqStream))
  1124. setOpsRequestContext(c, reqModel, reqStream)
  1125. setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(reqStream, false)))
  1126. if decision := h.checkSecurityAudit(c, reqLog, apiKey, subject, service.ContentModerationProtocolAnthropicMessages, reqModel, body); decision != nil && !decision.AllowNextStage {
  1127. h.anthropicSecurityAuditError(c, decision)
  1128. return
  1129. }
  1130. // 解析渠道级模型映射
  1131. channelMappingMsg, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, reqModel)
  1132. mappedBodyForMessages := newOpenAIModelMappedBodyCache(body, h.gatewayService.ReplaceModelInBody)
  1133. // 绑定错误透传服务,允许 service 层在非 failover 错误场景复用规则。
  1134. if h.errorPassthroughService != nil {
  1135. service.BindErrorPassthroughService(c, h.errorPassthroughService)
  1136. }
  1137. subscription, _ := middleware2.GetSubscriptionFromContext(c)
  1138. requestPlatform := openAICompatibleRequestPlatform(c.Request.Context(), apiKey)
  1139. service.SetOpsLatencyMs(c, service.OpsAuthLatencyMsKey, time.Since(requestStart).Milliseconds())
  1140. routingStart := time.Now()
  1141. userReleaseFunc, acquired := h.acquireResponsesUserSlot(c, subject.UserID, subject.Concurrency, reqStream, &streamStarted, reqLog)
  1142. if !acquired {
  1143. return
  1144. }
  1145. if userReleaseFunc != nil {
  1146. defer userReleaseFunc()
  1147. }
  1148. if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription, service.QuotaPlatform(c.Request.Context(), apiKey)); err != nil {
  1149. reqLog.Info("openai_messages.billing_eligibility_check_failed", zap.Error(err))
  1150. status, code, message, retryAfter := billingErrorDetails(err)
  1151. if retryAfter > 0 {
  1152. c.Header("Retry-After", strconv.Itoa(retryAfter))
  1153. }
  1154. h.anthropicStreamingAwareError(c, status, code, message, streamStarted)
  1155. return
  1156. }
  1157. sessionHash := h.gatewayService.GenerateSessionHash(c, body)
  1158. promptCacheKey := h.gatewayService.ExtractSessionID(c, body)
  1159. sessionHash, promptCacheKey = resolveOpenAIMessagesMetadataSession(c, sessionHash, promptCacheKey, reqModel, body)
  1160. if h.rejectIfCyberSessionBlocked(c, apiKey, body, reqModel, cyberBlockFormatAnthropic) {
  1161. return
  1162. }
  1163. maxAccountSwitches := h.maxAccountSwitches
  1164. switchCount := 0
  1165. profitVetoCount := 0
  1166. failedAccountIDs := make(map[int64]struct{})
  1167. sameAccountRetryCount := make(map[int64]int)
  1168. var lastFailoverErr *service.UpstreamFailoverError
  1169. var oauth429FailoverState service.OpenAIOAuth429FailoverState
  1170. effectiveMappedModel := preferredMappedModel
  1171. // 分组利润控制:Messages 文本入口同样请求级装门并固定 pricingAt。
  1172. msgPricingCtx, pricingAt := h.gatewayService.WithOpenAIRequestPricingContext(c.Request.Context(), apiKey.GroupID)
  1173. c.Request = c.Request.WithContext(msgPricingCtx)
  1174. for {
  1175. if failoverClientGone(c) {
  1176. return
  1177. }
  1178. currentRoutingModel := routingModel
  1179. if effectiveMappedModel != "" {
  1180. currentRoutingModel = effectiveMappedModel
  1181. }
  1182. reqLog.Debug("openai_messages.account_selecting", zap.Int("excluded_account_count", len(failedAccountIDs)))
  1183. selection, scheduleDecision, err := h.gatewayService.SelectAccountWithSchedulerForCapability(
  1184. c.Request.Context(),
  1185. apiKey.GroupID,
  1186. "", // no previous_response_id
  1187. sessionHash,
  1188. currentRoutingModel,
  1189. failedAccountIDs,
  1190. service.OpenAIUpstreamTransportAny,
  1191. service.OpenAIEndpointCapabilityChatCompletions,
  1192. false,
  1193. false,
  1194. true,
  1195. requestPlatform,
  1196. )
  1197. if err != nil {
  1198. if failoverClientGone(c) {
  1199. reqLog.Info("openai_messages.account_select_aborted_client_disconnected", zap.Error(err))
  1200. return
  1201. }
  1202. reqLog.Warn("openai_messages.account_select_failed",
  1203. zap.Error(openAICompatibleSelectionErrorForLog(err, requestPlatform)),
  1204. zap.Int("excluded_account_count", len(failedAccountIDs)),
  1205. )
  1206. if len(failedAccountIDs) == 0 {
  1207. if err != nil {
  1208. cls := classifyOpenAICompatibleNoAccountErrorFromGin(c, h.gatewayService, apiKey, currentRoutingModel, reqModel)
  1209. if !cls.ModelNotFound {
  1210. markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
  1211. }
  1212. h.anthropicStreamingAwareError(c, cls.Status, cls.ErrType, cls.Message, streamStarted)
  1213. return
  1214. }
  1215. } else {
  1216. if lastFailoverErr != nil {
  1217. h.handleAnthropicFailoverExhausted(c, lastFailoverErr, streamStarted)
  1218. } else {
  1219. h.anthropicStreamingAwareError(c, http.StatusBadGateway, "api_error", "Upstream request failed", streamStarted)
  1220. }
  1221. return
  1222. }
  1223. }
  1224. if selection == nil || selection.Account == nil {
  1225. cls := classifyOpenAICompatibleNoAccountErrorFromGin(c, h.gatewayService, apiKey, currentRoutingModel, reqModel)
  1226. if !cls.ModelNotFound {
  1227. markOpsRoutingCapacityLimited(c)
  1228. }
  1229. h.anthropicStreamingAwareError(c, cls.Status, cls.ErrType, cls.Message, streamStarted)
  1230. return
  1231. }
  1232. account := selection.Account
  1233. sessionHash = ensureOpenAIPoolModeSessionHash(sessionHash, account)
  1234. reqLog.Debug("openai_messages.account_selected", zap.Int64("account_id", account.ID), zap.String("account_name", account.Name))
  1235. _ = scheduleDecision
  1236. setOpsSelectedAccount(c, account.ID, account.Platform)
  1237. accountReleaseFunc, slotResult := h.acquireResponsesAccountSlot(c, apiKey.GroupID, sessionHash, selection, reqStream, &streamStarted, reqLog)
  1238. if slotResult == openAISlotAcquireProfitVetoed {
  1239. // 利润终检否决:排除该账号重新选号,全池耗尽由下一轮选号报错;
  1240. // 否决次数达上限则直接终止,避免排队抢槽后才终检的延迟放大。
  1241. if !recordOpenAIProfitVeto(failedAccountIDs, account.ID, &profitVetoCount) {
  1242. h.handleOpenAIProfitVetoExhausted(c, streamStarted, reqLog, profitVetoCount)
  1243. return
  1244. }
  1245. continue
  1246. }
  1247. if slotResult != openAISlotAcquireOK {
  1248. return
  1249. }
  1250. service.SetOpsLatencyMs(c, service.OpsRoutingLatencyMsKey, time.Since(routingStart).Milliseconds())
  1251. forwardStart := time.Now()
  1252. defaultMappedModel := strings.TrimSpace(effectiveMappedModel)
  1253. // 应用渠道模型映射到请求体
  1254. forwardBody := mappedBodyForMessages(channelMappingMsg.Mapped, channelMappingMsg.MappedModel)
  1255. writerSizeBeforeForward := c.Writer.Size()
  1256. result, err := func() (*service.OpenAIForwardResult, error) {
  1257. defer func() {
  1258. if accountReleaseFunc != nil {
  1259. accountReleaseFunc()
  1260. }
  1261. }()
  1262. return h.gatewayService.ForwardAsAnthropic(c.Request.Context(), c, account, forwardBody, promptCacheKey, defaultMappedModel)
  1263. }()
  1264. var cyberBlockBodyMsg []byte
  1265. if service.GetOpsCyberPolicy(c) != nil {
  1266. cyberBlockBodyMsg = body
  1267. }
  1268. h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, reqModel, err != nil, cyberBlockBodyMsg, clientRequestedUsageFields(c, channelMappingMsg, reqModel, ""), service.HashUsageRequestPayload(body))
  1269. forwardDurationMs := time.Since(forwardStart).Milliseconds()
  1270. upstreamLatencyMs, _ := getContextInt64(c, service.OpsUpstreamLatencyMsKey)
  1271. responseLatencyMs := forwardDurationMs
  1272. if upstreamLatencyMs > 0 && forwardDurationMs > upstreamLatencyMs {
  1273. responseLatencyMs = forwardDurationMs - upstreamLatencyMs
  1274. }
  1275. service.SetOpsLatencyMs(c, service.OpsResponseLatencyMsKey, responseLatencyMs)
  1276. if err == nil && result != nil && result.FirstTokenMs != nil {
  1277. service.SetOpsLatencyMs(c, service.OpsTimeToFirstTokenMsKey, int64(*result.FirstTokenMs))
  1278. }
  1279. // Forward 与错误一起返回的部分结果:流中断/客户端断开排水前上游已计量的
  1280. // usage 照常入账,避免上游已产生消耗的请求完全漏记(#5148,对齐 anthropic
  1281. // 网关同名修复)。failover 错误恒定 result=nil,不会重复计费。
  1282. submitMessagesUsage := func(res *service.OpenAIForwardResult) {
  1283. if res == nil {
  1284. return
  1285. }
  1286. stampOpenAIRequestedReasoningEffort(res, c)
  1287. userAgent := c.GetHeader("User-Agent")
  1288. clientIP := ip.GetClientIP(c)
  1289. requestPayloadHash := service.HashUsageRequestPayload(body)
  1290. inboundEndpoint := GetInboundEndpoint(c)
  1291. upstreamEndpoint := resolveOpenAIUpstreamEndpoint(c, account, res)
  1292. quotaPlatform := service.QuotaPlatform(c.Request.Context(), apiKey)
  1293. sessionID := service.ExtractClientSessionID(c)
  1294. cyberBlocked := service.GetOpsCyberPolicy(c) != nil
  1295. h.submitOpenAIUsageRecordTask(c.Request.Context(), res, func(ctx context.Context) {
  1296. if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{
  1297. Result: res,
  1298. APIKey: apiKey,
  1299. User: apiKey.User,
  1300. Account: account,
  1301. Subscription: subscription,
  1302. InboundEndpoint: inboundEndpoint,
  1303. UpstreamEndpoint: upstreamEndpoint,
  1304. UserAgent: userAgent,
  1305. IPAddress: clientIP,
  1306. RequestPayloadHash: requestPayloadHash,
  1307. APIKeyService: h.apiKeyService,
  1308. QuotaPlatform: quotaPlatform,
  1309. SessionID: sessionID,
  1310. ChannelUsageFields: clientRequestedUsageFields(c, channelMappingMsg, reqModel, res.UpstreamModel),
  1311. PricingAt: pricingAt,
  1312. CyberBlocked: cyberBlocked,
  1313. }); err != nil {
  1314. logger.L().With(
  1315. zap.String("component", "handler.openai_gateway.messages"),
  1316. zap.Int64("user_id", subject.UserID),
  1317. zap.Int64("api_key_id", apiKey.ID),
  1318. zap.Any("group_id", apiKey.GroupID),
  1319. zap.String("model", reqModel),
  1320. zap.Int64("account_id", account.ID),
  1321. ).Error("openai_messages.record_usage_failed", zap.Error(err))
  1322. }
  1323. })
  1324. }
  1325. if err != nil {
  1326. if result != nil && result.ImageCount > 0 {
  1327. reqLog.Warn("openai_messages.forward_partial_error_with_image_result",
  1328. zap.Int64("account_id", account.ID),
  1329. zap.Int("image_count", result.ImageCount),
  1330. zap.Error(err),
  1331. )
  1332. } else {
  1333. var failoverErr *service.UpstreamFailoverError
  1334. if errors.As(err, &failoverErr) {
  1335. if failoverClientGone(c) {
  1336. reqLog.Info("openai_messages.failover_aborted_client_disconnected",
  1337. zap.Int64("account_id", account.ID),
  1338. zap.Int("upstream_status", failoverErr.StatusCode),
  1339. )
  1340. return
  1341. }
  1342. if c.Writer.Size() != writerSizeBeforeForward {
  1343. h.gatewayService.ObserveOpenAIAccountHealthFailure(c.Request.Context(), account, err)
  1344. h.handleAnthropicFailoverExhausted(c, failoverErr, true)
  1345. return
  1346. }
  1347. if failoverErr.ShouldReportAccountScheduleFailure() {
  1348. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, currentRoutingModel, false, nil), false, nil, err)
  1349. }
  1350. if !failoverErr.ShouldRetryNextAccount() {
  1351. h.handleAnthropicFailoverExhausted(c, failoverErr, streamStarted)
  1352. return
  1353. }
  1354. // 池模式:同账号重试
  1355. if failoverErr.RetryableOnSameAccount {
  1356. retryLimit := effectiveSameAccountRetryLimit(failoverErr, account)
  1357. if sameAccountRetryAllowed(failoverErr, sameAccountRetryCount[account.ID], retryLimit) {
  1358. sameAccountRetryCount[account.ID]++
  1359. retryDelay := sameAccountRetryDelayFor(failoverErr, sameAccountRetryCount[account.ID])
  1360. reqLog.Warn("openai_messages.pool_mode_same_account_retry",
  1361. zap.Int64("account_id", account.ID),
  1362. zap.Int("upstream_status", failoverErr.StatusCode),
  1363. zap.Int("retry_limit", retryLimit),
  1364. zap.Int("retry_count", sameAccountRetryCount[account.ID]),
  1365. zap.Duration("retry_delay", retryDelay),
  1366. )
  1367. select {
  1368. case <-c.Request.Context().Done():
  1369. return
  1370. case <-time.After(retryDelay):
  1371. }
  1372. continue
  1373. }
  1374. }
  1375. h.gatewayService.RecordOpenAIAccountSwitch()
  1376. failedAccountIDs[account.ID] = struct{}{}
  1377. lastFailoverErr = failoverErr
  1378. if switchCount >= maxAccountSwitches {
  1379. h.handleAnthropicFailoverExhausted(c, failoverErr, streamStarted)
  1380. return
  1381. }
  1382. switchCount++
  1383. if h.gatewayService.ShouldStopOpenAIOAuth429Failover(account, failoverErr.StatusCode, switchCount, &oauth429FailoverState) {
  1384. h.handleAnthropicFailoverExhausted(c, failoverErr, streamStarted)
  1385. return
  1386. }
  1387. reqLog.Warn("openai_messages.upstream_failover_switching",
  1388. zap.Int64("account_id", account.ID),
  1389. zap.Int("upstream_status", failoverErr.StatusCode),
  1390. zap.Int("switch_count", switchCount),
  1391. zap.Int("max_switches", maxAccountSwitches),
  1392. )
  1393. continue
  1394. }
  1395. if result != nil && result.ClientDisconnect {
  1396. reqLog.Info("openai_messages.client_disconnected",
  1397. zap.Int64("account_id", account.ID),
  1398. zap.Error(err),
  1399. )
  1400. // 断开排水期间上游已计量的 usage 必须入账(此前直接 return 丢弃,
  1401. // payg 上游照常计费而平台漏记)。
  1402. submitMessagesUsage(result)
  1403. return
  1404. }
  1405. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, currentRoutingModel, false, result), false, nil, err)
  1406. wroteFallback := h.ensureAnthropicErrorResponse(c, streamStarted)
  1407. reqLog.Warn("openai_messages.forward_failed",
  1408. zap.Int64("account_id", account.ID),
  1409. zap.Bool("fallback_error_response_written", wroteFallback),
  1410. zap.Error(err),
  1411. )
  1412. submitMessagesUsage(result)
  1413. return
  1414. }
  1415. }
  1416. if result != nil {
  1417. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, currentRoutingModel, false, result), true, result.FirstTokenMs)
  1418. } else {
  1419. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, currentRoutingModel, false, result), true, nil)
  1420. }
  1421. submitMessagesUsage(result)
  1422. reqLog.Debug("openai_messages.request_completed",
  1423. zap.Int64("account_id", account.ID),
  1424. zap.Int("switch_count", switchCount),
  1425. )
  1426. return
  1427. }
  1428. }
  1429. func resolveOpenAIMessagesMetadataSession(c *gin.Context, sessionHash, promptCacheKey, reqModel string, body []byte) (string, string) {
  1430. // Anthropic metadata.user_id 只作为账号粘性信号。上游 GPT/Codex 缓存键
  1431. // 交给 ForwardAsAnthropic 从 cache_control 或完整消息 digest 派生,避免
  1432. // 固定 metadata key 压住后续 turn 的缓存滚动。
  1433. //
  1434. // Claude Code 的 X-Claude-Code-Session-Id 是比 body content fallback 更稳定的
  1435. // 会话边界,但它只用于本地账号粘性;不要把它提升为 prompt_cache_key 或上游
  1436. // session_id,否则会改变现有 Messages→Codex 缓存滚动语义。
  1437. if promptCacheKey == "" {
  1438. if claudeSessionID := service.ClaudeCodeSessionIDFromHeader(c); claudeSessionID != "" {
  1439. return service.DeriveSessionHashFromSeed(claudeSessionID), promptCacheKey
  1440. }
  1441. }
  1442. if sessionHash != "" {
  1443. return sessionHash, promptCacheKey
  1444. }
  1445. if userID := strings.TrimSpace(gjson.GetBytes(body, "metadata.user_id").String()); userID != "" {
  1446. seed := reqModel + "-" + userID
  1447. sessionHash = service.DeriveSessionHashFromSeed(seed)
  1448. }
  1449. return sessionHash, promptCacheKey
  1450. }
  1451. // anthropicErrorResponse writes an error in Anthropic Messages API format.
  1452. func (h *OpenAIGatewayHandler) anthropicErrorResponse(c *gin.Context, status int, errType, message string) {
  1453. c.JSON(status, gin.H{
  1454. "type": "error",
  1455. "error": gin.H{
  1456. "type": errType,
  1457. "message": message,
  1458. },
  1459. })
  1460. }
  1461. // anthropicStreamingAwareError handles errors that may occur during streaming,
  1462. // using Anthropic SSE error format.
  1463. func (h *OpenAIGatewayHandler) anthropicStreamingAwareError(c *gin.Context, status int, errType, message string, streamStarted bool) {
  1464. if streamStarted {
  1465. flusher, ok := c.Writer.(http.Flusher)
  1466. if ok {
  1467. errPayload, _ := json.Marshal(gin.H{
  1468. "type": "error",
  1469. "error": gin.H{
  1470. "type": errType,
  1471. "message": message,
  1472. },
  1473. })
  1474. fmt.Fprintf(c.Writer, "event: error\ndata: %s\n\n", errPayload) //nolint:errcheck
  1475. flusher.Flush()
  1476. }
  1477. return
  1478. }
  1479. h.anthropicErrorResponse(c, status, errType, message)
  1480. }
  1481. // handleAnthropicFailoverExhausted maps upstream failover errors to Anthropic format.
  1482. func (h *OpenAIGatewayHandler) handleAnthropicFailoverExhausted(c *gin.Context, failoverErr *service.UpstreamFailoverError, streamStarted bool) {
  1483. if failoverErr != nil {
  1484. copyFailoverRetryAfter(c, failoverErr.ResponseHeaders)
  1485. }
  1486. if failoverErr != nil && failoverErr.IsCredentialFailure() {
  1487. status, message := credentialFailoverClientResponse(failoverErr)
  1488. h.anthropicStreamingAwareError(c, status, "api_error", message, streamStarted)
  1489. return
  1490. }
  1491. if failoverErr != nil && failoverErr.IsOpenAICapacityShed() && strings.TrimSpace(failoverErr.ClientMessage) != "" {
  1492. status := failoverErr.ClientStatusCode
  1493. if status <= 0 {
  1494. status = http.StatusServiceUnavailable
  1495. }
  1496. h.anthropicStreamingAwareError(c, status, "api_error", failoverErr.ClientMessage, streamStarted)
  1497. return
  1498. }
  1499. status, errType, errMsg := h.mapUpstreamError(failoverErr.StatusCode)
  1500. h.anthropicStreamingAwareError(c, status, errType, errMsg, streamStarted)
  1501. }
  1502. // ensureAnthropicErrorResponse writes a fallback Anthropic error if no response was written.
  1503. func (h *OpenAIGatewayHandler) ensureAnthropicErrorResponse(c *gin.Context, streamStarted bool) bool {
  1504. if c == nil || c.Writer == nil || c.Writer.Written() {
  1505. return false
  1506. }
  1507. h.anthropicStreamingAwareError(c, http.StatusBadGateway, "api_error", "Upstream request failed", streamStarted)
  1508. return true
  1509. }
  1510. func (h *OpenAIGatewayHandler) validateFunctionCallOutputRequest(c *gin.Context, body []byte, reqLog *zap.Logger) bool {
  1511. if !gjson.GetBytes(body, `input.#(type=="function_call_output")`).Exists() {
  1512. return true
  1513. }
  1514. validation := service.ValidateFunctionCallOutputContextBytes(body)
  1515. if !validation.HasFunctionCallOutput {
  1516. return true
  1517. }
  1518. previousResponseID := gjson.GetBytes(body, "previous_response_id").String()
  1519. if strings.TrimSpace(previousResponseID) != "" || validation.HasToolCallContext {
  1520. return true
  1521. }
  1522. if validation.HasFunctionCallOutputMissingCallID {
  1523. reqLog.Warn("openai.request_validation_failed",
  1524. zap.String("reason", "function_call_output_missing_call_id"),
  1525. )
  1526. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "function_call_output requires call_id on HTTP requests; continuation via previous_response_id is only supported on Responses WebSocket v2")
  1527. return false
  1528. }
  1529. if validation.HasItemReferenceForAllCallIDs {
  1530. return true
  1531. }
  1532. reqLog.Warn("openai.request_validation_failed",
  1533. zap.String("reason", "function_call_output_missing_item_reference"),
  1534. )
  1535. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "function_call_output requires item_reference ids matching each call_id on HTTP requests; continuation via previous_response_id is only supported on Responses WebSocket v2")
  1536. return false
  1537. }
  1538. func normalizeCodexDelegationBootstrap(body []byte) ([]byte, bool) {
  1539. // 已有任务通过 send_message_to_thread 唤醒时会携带 previous_response_id;
  1540. // 完整历史回放还会带有已配对的调用项。delegation 仍是客户端注入的用户输入,
  1541. // 不属于这些历史调用的结果,因此允许它与可明确配对的历史上下文共存。
  1542. return normalizeCodexCallOutputBootstrap(body, isCodexDelegationCandidate, true)
  1543. }
  1544. func normalizeCodexAutomationBootstrap(body []byte) ([]byte, bool) {
  1545. return normalizeCodexCallOutputBootstrap(body, isCodexAutomationCandidate, false)
  1546. }
  1547. func normalizeCodexCallOutputBootstrap(body []byte, isCandidate func(map[string]any) bool, allowHistoricalContext bool) ([]byte, bool) {
  1548. if !hasUniqueJSONMembers(body) {
  1549. return body, false
  1550. }
  1551. decoder := json.NewDecoder(bytes.NewReader(body))
  1552. decoder.UseNumber()
  1553. var request map[string]any
  1554. if err := decoder.Decode(&request); err != nil {
  1555. return body, false
  1556. }
  1557. if previousResponseID, exists := request["previous_response_id"]; exists {
  1558. value, ok := previousResponseID.(string)
  1559. if !ok || (!allowHistoricalContext && strings.TrimSpace(value) != "") {
  1560. return body, false
  1561. }
  1562. }
  1563. input, ok := request["input"].([]any)
  1564. if !ok {
  1565. return body, false
  1566. }
  1567. // Responses built-ins follow the *_call / *_call_output naming convention,
  1568. // so classify by the wire type shape instead of maintaining an incomplete
  1569. // allowlist. Delegation may coexist with historical anchors only when their
  1570. // IDs make them unambiguous; automation retains the bootstrap-only boundary.
  1571. for _, raw := range input {
  1572. item, ok := raw.(map[string]any)
  1573. if !ok {
  1574. continue
  1575. }
  1576. typ := stringField(item, "type")
  1577. if isCandidate(item) {
  1578. callIDValue, exists := item["call_id"]
  1579. callID, isString := callIDValue.(string)
  1580. if exists && (!isString || strings.TrimSpace(callID) != "") {
  1581. return body, false
  1582. }
  1583. continue
  1584. }
  1585. if typ == "item_reference" {
  1586. if allowHistoricalContext && strings.TrimSpace(stringField(item, "id")) != "" {
  1587. continue
  1588. }
  1589. return body, false
  1590. }
  1591. if strings.HasSuffix(typ, "_call") || isResponsesCallOutputType(typ) {
  1592. if allowHistoricalContext && strings.TrimSpace(stringField(item, "call_id")) != "" {
  1593. continue
  1594. }
  1595. return body, false
  1596. }
  1597. }
  1598. changed := false
  1599. for i, raw := range input {
  1600. item, ok := raw.(map[string]any)
  1601. if !ok || !isCandidate(item) {
  1602. continue
  1603. }
  1604. output, ok := item["output"].(string)
  1605. if !ok {
  1606. continue
  1607. }
  1608. input[i] = map[string]any{
  1609. "type": "message",
  1610. "role": "user",
  1611. "content": []any{map[string]any{
  1612. "type": "input_text",
  1613. "text": output,
  1614. }},
  1615. }
  1616. changed = true
  1617. }
  1618. if !changed {
  1619. return body, false
  1620. }
  1621. normalized, err := json.Marshal(request)
  1622. if err != nil {
  1623. return body, false
  1624. }
  1625. return normalized, true
  1626. }
  1627. func hasUniqueJSONMembers(body []byte) bool {
  1628. decoder := json.NewDecoder(bytes.NewReader(body))
  1629. if !consumeUniqueJSONValue(decoder) {
  1630. return false
  1631. }
  1632. _, err := decoder.Token()
  1633. return err == io.EOF
  1634. }
  1635. func consumeUniqueJSONValue(decoder *json.Decoder) bool {
  1636. token, err := decoder.Token()
  1637. if err != nil {
  1638. return false
  1639. }
  1640. delim, ok := token.(json.Delim)
  1641. if !ok {
  1642. return true
  1643. }
  1644. switch delim {
  1645. case '{':
  1646. members := make(map[string]struct{})
  1647. for decoder.More() {
  1648. keyToken, err := decoder.Token()
  1649. if err != nil {
  1650. return false
  1651. }
  1652. key, ok := keyToken.(string)
  1653. if !ok {
  1654. return false
  1655. }
  1656. if _, duplicate := members[key]; duplicate {
  1657. return false
  1658. }
  1659. members[key] = struct{}{}
  1660. if !consumeUniqueJSONValue(decoder) {
  1661. return false
  1662. }
  1663. }
  1664. end, err := decoder.Token()
  1665. return err == nil && end == json.Delim('}')
  1666. case '[':
  1667. for decoder.More() {
  1668. if !consumeUniqueJSONValue(decoder) {
  1669. return false
  1670. }
  1671. }
  1672. end, err := decoder.Token()
  1673. return err == nil && end == json.Delim(']')
  1674. default:
  1675. return false
  1676. }
  1677. }
  1678. func isResponsesCallOutputType(typ string) bool {
  1679. return strings.HasSuffix(typ, "_call_output") || typ == "tool_search_output"
  1680. }
  1681. func isCodexDelegationCandidate(item map[string]any) bool {
  1682. if stringField(item, "type") != "function_call_output" ||
  1683. !isCodexDelegationTool(stringField(item, "namespace"), stringField(item, "name")) {
  1684. return false
  1685. }
  1686. output, ok := item["output"].(string)
  1687. return ok && validCodexDelegationEnvelope(output)
  1688. }
  1689. func isCodexAutomationCandidate(item map[string]any) bool {
  1690. if stringField(item, "type") != "function_call_output" ||
  1691. stringField(item, "namespace") != "codex_app" ||
  1692. stringField(item, "name") != "automation_update" {
  1693. return false
  1694. }
  1695. output, ok := item["output"].(string)
  1696. return ok && (validCodexAutomationBootstrap(output) || validCodexAutomationHeartbeat(output))
  1697. }
  1698. func stringField(item map[string]any, key string) string {
  1699. value, _ := item[key].(string)
  1700. return value
  1701. }
  1702. func isCodexDelegationTool(namespace, name string) bool {
  1703. return (namespace == "codex_app" || namespace == "codex_tui") &&
  1704. (name == "create_thread" || name == "send_message_to_thread")
  1705. }
  1706. func validCodexAutomationBootstrap(value string) bool {
  1707. normalized := strings.ReplaceAll(value, "\r\n", "\n")
  1708. if strings.ContainsRune(normalized, '\r') {
  1709. return false
  1710. }
  1711. lines := strings.Split(normalized, "\n")
  1712. if len(lines) < 6 {
  1713. return false
  1714. }
  1715. if _, ok := codexAutomationHeaderValue(lines[0], "Automation: "); !ok {
  1716. return false
  1717. }
  1718. automationID, ok := codexAutomationHeaderValue(lines[1], "Automation ID: ")
  1719. if !ok || !validCodexAutomationID(automationID) {
  1720. return false
  1721. }
  1722. expectedMemory := "Automation memory: $CODEX_HOME/automations/" + automationID + "/memory.md"
  1723. if lines[2] != expectedMemory {
  1724. return false
  1725. }
  1726. lastRun, ok := codexAutomationHeaderValue(lines[3], "Last run: ")
  1727. if !ok || !validCodexAutomationLastRun(lastRun) || lines[4] != "" {
  1728. return false
  1729. }
  1730. return strings.TrimSpace(strings.Join(lines[5:], "\n")) != ""
  1731. }
  1732. func codexAutomationHeaderValue(line, prefix string) (string, bool) {
  1733. if !strings.HasPrefix(line, prefix) {
  1734. return "", false
  1735. }
  1736. value := strings.TrimPrefix(line, prefix)
  1737. return value, value != "" && strings.TrimSpace(value) == value
  1738. }
  1739. func validCodexAutomationID(value string) bool {
  1740. if len(value) == 0 || len(value) > 128 || value == "." || value == ".." {
  1741. return false
  1742. }
  1743. for i := 0; i < len(value); i++ {
  1744. c := value[i]
  1745. if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' {
  1746. continue
  1747. }
  1748. return false
  1749. }
  1750. return true
  1751. }
  1752. func validCodexAutomationLastRun(value string) bool {
  1753. if value == "never" {
  1754. return true
  1755. }
  1756. separator := strings.LastIndex(value, " (")
  1757. if separator <= 0 || !strings.HasSuffix(value, ")") {
  1758. return false
  1759. }
  1760. runAt, err := time.Parse(time.RFC3339Nano, value[:separator])
  1761. if err != nil {
  1762. return false
  1763. }
  1764. epochMillis, err := strconv.ParseInt(value[separator+2:len(value)-1], 10, 64)
  1765. return err == nil && runAt.UnixMilli() == epochMillis
  1766. }
  1767. func validCodexAutomationHeartbeat(value string) bool {
  1768. decoder := xml.NewDecoder(strings.NewReader(value))
  1769. var rootSeen, automationIDSeen bool
  1770. var automationID bytes.Buffer
  1771. depth := 0
  1772. for {
  1773. token, err := decoder.Token()
  1774. if err == io.EOF {
  1775. id := automationID.String()
  1776. return rootSeen && automationIDSeen && depth == 0 &&
  1777. strings.TrimSpace(id) == id && validCodexAutomationID(id)
  1778. }
  1779. if err != nil {
  1780. return false
  1781. }
  1782. switch current := token.(type) {
  1783. case xml.StartElement:
  1784. depth++
  1785. if current.Name.Space != "" || len(current.Attr) != 0 || depth > 2 {
  1786. return false
  1787. }
  1788. if depth == 1 {
  1789. if rootSeen || current.Name.Local != "heartbeat" {
  1790. return false
  1791. }
  1792. rootSeen = true
  1793. } else if automationIDSeen || current.Name.Local != "automation_id" {
  1794. return false
  1795. }
  1796. automationIDSeen = depth == 2
  1797. case xml.EndElement:
  1798. if current.Name.Space != "" {
  1799. return false
  1800. }
  1801. depth--
  1802. if depth < 0 {
  1803. return false
  1804. }
  1805. case xml.CharData:
  1806. if depth == 2 {
  1807. _, _ = automationID.Write(current)
  1808. } else if len(bytes.TrimSpace(current)) != 0 {
  1809. return false
  1810. }
  1811. case xml.Comment, xml.ProcInst, xml.Directive:
  1812. return false
  1813. }
  1814. }
  1815. }
  1816. func validCodexDelegationEnvelope(value string) bool {
  1817. decoder := xml.NewDecoder(strings.NewReader(value))
  1818. var rootSeen, sourceSeen, inputSeen bool
  1819. var childName string
  1820. var childText bytes.Buffer
  1821. depth := 0
  1822. for {
  1823. token, err := decoder.Token()
  1824. if err == io.EOF {
  1825. return rootSeen && depth == 0 && sourceSeen && inputSeen
  1826. }
  1827. if err != nil {
  1828. return false
  1829. }
  1830. switch current := token.(type) {
  1831. case xml.StartElement:
  1832. depth++
  1833. if current.Name.Space != "" || len(current.Attr) != 0 || (depth == 1 && current.Name.Local != "codex_delegation") || depth > 2 {
  1834. return false
  1835. }
  1836. if depth == 1 {
  1837. if rootSeen {
  1838. return false
  1839. }
  1840. rootSeen = true
  1841. continue
  1842. }
  1843. if current.Name.Local != "source_thread_id" && current.Name.Local != "input" {
  1844. return false
  1845. }
  1846. childName = current.Name.Local
  1847. childText.Reset()
  1848. case xml.EndElement:
  1849. if current.Name.Space != "" {
  1850. return false
  1851. }
  1852. if depth == 2 {
  1853. if current.Name.Local != childName || strings.TrimSpace(childText.String()) == "" {
  1854. return false
  1855. }
  1856. if childName == "source_thread_id" {
  1857. if sourceSeen {
  1858. return false
  1859. }
  1860. sourceSeen = true
  1861. } else {
  1862. if inputSeen {
  1863. return false
  1864. }
  1865. inputSeen = true
  1866. }
  1867. childName = ""
  1868. }
  1869. depth--
  1870. if depth < 0 {
  1871. return false
  1872. }
  1873. case xml.CharData:
  1874. if depth == 2 {
  1875. _, _ = childText.Write(current)
  1876. } else if len(bytes.TrimSpace(current)) != 0 {
  1877. return false
  1878. }
  1879. case xml.Comment:
  1880. return false
  1881. case xml.ProcInst, xml.Directive:
  1882. return false
  1883. }
  1884. }
  1885. }
  1886. func (h *OpenAIGatewayHandler) acquireResponsesUserSlot(
  1887. c *gin.Context,
  1888. userID int64,
  1889. userConcurrency int,
  1890. reqStream bool,
  1891. streamStarted *bool,
  1892. reqLog *zap.Logger,
  1893. ) (func(), bool) {
  1894. ctx := c.Request.Context()
  1895. userReleaseFunc, err := h.concurrencyHelper.AcquireUserSlotWithWait(c, userID, userConcurrency, reqStream, streamStarted)
  1896. if err != nil {
  1897. reqLog.Warn("openai.user_slot_acquire_failed", zap.Error(err))
  1898. h.handleConcurrencyError(c, err, "user", *streamStarted)
  1899. return nil, false
  1900. }
  1901. return wrapReleaseOnDone(ctx, userReleaseFunc), true
  1902. }
  1903. // openAISlotAcquireResult 是账号槽位获取的三态结果。
  1904. type openAISlotAcquireResult int
  1905. const (
  1906. openAISlotAcquireOK openAISlotAcquireResult = iota
  1907. // openAISlotAcquireFailed:错误响应已写出,调用方直接 return。
  1908. openAISlotAcquireFailed
  1909. // openAISlotAcquireProfitVetoed:槽位获取成功后利润终检否决。槽位已释放、
  1910. // 未写任何响应;调用方应经 recordOpenAIProfitVeto 把该账号加入本请求排除集
  1911. // 后重新选号,全池耗尽由下一轮选号返回标准 no available accounts。
  1912. openAISlotAcquireProfitVetoed
  1913. )
  1914. // openAIWSTurnPricing 持有 WebSocket 连接内「当前 turn」的计费定价时刻。
  1915. // 由 BeforeTurn 在每个 turn 开始时冻结,AfterTurn 的用量提交读取它;turn 在
  1916. // 连接内串行推进,互斥锁只为跨用量提交 goroutine 的读取安全。
  1917. //
  1918. // 零值语义(重要):首轮准入由握手路径完成,不调用 BeforeTurn,因此首轮保持
  1919. // 零值并回退到 TurnStarted 记录的首轮开始时刻。后续 turn 在 response.create
  1920. // 写入上游前调用 BeforeTurn,按当时的利润门复核并冻结定价。绝不能用建连时刻
  1921. // 初始化,否则会把长连接的所有 turn 钉死在建连时的峰谷因子。
  1922. type openAIWSTurnPricing struct {
  1923. mu sync.Mutex
  1924. at time.Time
  1925. }
  1926. func (p *openAIWSTurnPricing) freeze(at time.Time) {
  1927. p.mu.Lock()
  1928. p.at = at
  1929. p.mu.Unlock()
  1930. }
  1931. func (p *openAIWSTurnPricing) currentOr(fallback time.Time) time.Time {
  1932. p.mu.Lock()
  1933. defer p.mu.Unlock()
  1934. if !p.at.IsZero() {
  1935. return p.at
  1936. }
  1937. return fallback
  1938. }
  1939. // recordOpenAIProfitVeto 记录 OpenAI 侧选号循环的一次利润门终检否决:把账号
  1940. // 加入本请求排除集并递增否决计数。返回 false 表示否决次数已达
  1941. // maxProfitVetoAttempts,调用方必须停止重选并按「无可用账号」终止。
  1942. //
  1943. // OpenAI 路径用的是各自的 failedAccountIDs map + for 循环(不是 FailoverState),
  1944. // 这里用一个独立计数器复用同一上限语义。上限是必需的:WaitPlan 分支先阻塞
  1945. // 排队(sticky 45s / fallback 30s)拿到槽位才终检,无上限重选会把单次请求的
  1946. // 延迟放大到 N × WaitPlan.Timeout。
  1947. func recordOpenAIProfitVeto(failedAccountIDs map[int64]struct{}, accountID int64, vetoCount *int) bool {
  1948. failedAccountIDs[accountID] = struct{}{}
  1949. *vetoCount++
  1950. return *vetoCount < maxProfitVetoAttempts
  1951. }
  1952. // handleOpenAIProfitVetoExhausted 在利润否决预算耗尽时写出错误响应。
  1953. // 与 acquireResponsesAccountSlot 内部的 no-available-accounts 失败分支同形,
  1954. // 保证同一调用方在两条路径上拿到一致的响应格式。
  1955. func (h *OpenAIGatewayHandler) handleOpenAIProfitVetoExhausted(
  1956. c *gin.Context,
  1957. streamStarted bool,
  1958. reqLog *zap.Logger,
  1959. vetoCount int,
  1960. ) {
  1961. reqLog.Warn("openai.profit_veto_attempts_exhausted", zap.Int("profit_veto_count", vetoCount))
  1962. markOpsRoutingCapacityLimited(c)
  1963. h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", profitVetoExhaustedMessage, streamStarted)
  1964. }
  1965. func (h *OpenAIGatewayHandler) acquireResponsesAccountSlot(
  1966. c *gin.Context,
  1967. groupID *int64,
  1968. sessionHash string,
  1969. selection *service.AccountSelectionResult,
  1970. reqStream bool,
  1971. streamStarted *bool,
  1972. reqLog *zap.Logger,
  1973. ) (func(), openAISlotAcquireResult) {
  1974. return h.acquireOpenAIAccountSlot(c, groupID, sessionHash, selection, reqStream, streamStarted, reqLog, nil)
  1975. }
  1976. type openAISlotErrorWriter func(status int, errType, code, message string)
  1977. // acquireOpenAIAccountSlot centralizes scheduler selection admission. The
  1978. // optional error writer lets non-Responses endpoints retain their wire format
  1979. // while sharing the same WaitPlan, cancellation, and release semantics.
  1980. func (h *OpenAIGatewayHandler) acquireOpenAIAccountSlot(
  1981. c *gin.Context,
  1982. groupID *int64,
  1983. sessionHash string,
  1984. selection *service.AccountSelectionResult,
  1985. reqStream bool,
  1986. streamStarted *bool,
  1987. reqLog *zap.Logger,
  1988. writeError openAISlotErrorWriter,
  1989. ) (func(), openAISlotAcquireResult) {
  1990. if writeError == nil {
  1991. writeError = func(status int, errType, code, message string) {
  1992. h.handleStreamingAwareErrorWithCode(c, status, errType, code, message, *streamStarted, false)
  1993. }
  1994. }
  1995. if selection == nil || selection.Account == nil {
  1996. markOpsRoutingCapacityLimited(c)
  1997. writeError(http.StatusServiceUnavailable, "api_error", "", "No available accounts")
  1998. return nil, openAISlotAcquireFailed
  1999. }
  2000. // 终检与准入后绑定使用选号结果携带的门:composite 等跨分组调度解析出的
  2001. // 门只存在于调度栈的局部 ctx,必须经选号结果重放到本函数的 ctx 上。
  2002. ctx := service.ContextWithSelectionProfitGate(c.Request.Context(), selection)
  2003. account := selection.Account
  2004. if selection.Acquired {
  2005. latest, vetoed, reason := h.gatewayService.ProfitControlVetoLatest(ctx, account)
  2006. if vetoed {
  2007. if selection.ReleaseFunc != nil {
  2008. selection.ReleaseFunc()
  2009. }
  2010. reqLog.Debug("openai.account_slot_profit_vetoed", zap.Int64("account_id", account.ID), zap.String("reason", reason))
  2011. return nil, openAISlotAcquireProfitVetoed
  2012. }
  2013. account = latest
  2014. selection.Account = latest
  2015. // 调度器已抢槽路径无门时由选号内部完成 eager 绑定;门下选号内部
  2016. // 推迟绑定,这里在终检通过后补准入后绑定。
  2017. if selection.ProfitGateActive() {
  2018. if err := h.gatewayService.BindStickySessionAfterProfitAdmission(ctx, groupID, sessionHash, account.ID); err != nil {
  2019. reqLog.Warn("openai.bind_sticky_session_after_profit_admission_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  2020. }
  2021. }
  2022. return wrapReleaseOnDone(ctx, selection.ReleaseFunc), openAISlotAcquireOK
  2023. }
  2024. if selection.WaitPlan == nil {
  2025. markOpsRoutingCapacityLimited(c)
  2026. writeError(http.StatusServiceUnavailable, "api_error", "", "No available accounts")
  2027. return nil, openAISlotAcquireFailed
  2028. }
  2029. fastReleaseFunc, fastAcquired, err := h.concurrencyHelper.TryAcquireAccountSlot(
  2030. ctx,
  2031. account.ID,
  2032. selection.WaitPlan.MaxConcurrency,
  2033. )
  2034. if err != nil {
  2035. reqLog.Warn("openai.account_slot_quick_acquire_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  2036. status, errType, code, message := concurrencyErrorResponse(err, "account")
  2037. writeError(status, errType, code, message)
  2038. return nil, openAISlotAcquireFailed
  2039. }
  2040. if fastAcquired {
  2041. // 分组利润控制:快速抢槽成功后终检。选号与抢槽之间账号
  2042. // 倍率可能刷新,越线则释放槽位交由调用方排除重选,不绑定粘连。
  2043. latest, vetoed, reason := h.gatewayService.ProfitControlVetoLatest(ctx, account)
  2044. if vetoed {
  2045. if fastReleaseFunc != nil {
  2046. fastReleaseFunc()
  2047. }
  2048. reqLog.Debug("openai.account_slot_profit_vetoed", zap.Int64("account_id", account.ID), zap.String("reason", reason))
  2049. return nil, openAISlotAcquireProfitVetoed
  2050. }
  2051. account = latest
  2052. selection.Account = latest
  2053. if err := h.gatewayService.BindStickySessionAfterProfitAdmission(ctx, groupID, sessionHash, account.ID); err != nil {
  2054. reqLog.Warn("openai.bind_sticky_session_after_profit_admission_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  2055. }
  2056. return wrapReleaseOnDone(ctx, fastReleaseFunc), openAISlotAcquireOK
  2057. }
  2058. canWait, waitErr := h.concurrencyHelper.IncrementAccountWaitCount(ctx, account.ID, selection.WaitPlan.MaxWaiting)
  2059. if waitErr != nil {
  2060. reqLog.Warn("openai.account_wait_counter_increment_failed", zap.Int64("account_id", account.ID), zap.Error(waitErr))
  2061. } else if !canWait {
  2062. reqLog.Info("openai.account_wait_queue_full",
  2063. zap.Int64("account_id", account.ID),
  2064. zap.Int("max_waiting", selection.WaitPlan.MaxWaiting),
  2065. )
  2066. writeError(http.StatusTooManyRequests, "rate_limit_error", gatewayQueueFullCode, "Too many pending requests, please retry later")
  2067. return nil, openAISlotAcquireFailed
  2068. }
  2069. accountWaitCounted := waitErr == nil && canWait
  2070. releaseWait := func() {
  2071. if accountWaitCounted {
  2072. h.concurrencyHelper.DecrementAccountWaitCount(ctx, account.ID)
  2073. accountWaitCounted = false
  2074. }
  2075. }
  2076. defer releaseWait()
  2077. accountReleaseFunc, err := h.concurrencyHelper.AcquireAccountSlotWithWaitTimeout(
  2078. c,
  2079. account.ID,
  2080. selection.WaitPlan.MaxConcurrency,
  2081. selection.WaitPlan.Timeout,
  2082. reqStream,
  2083. streamStarted,
  2084. )
  2085. if err != nil {
  2086. reqLog.Warn("openai.account_slot_acquire_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  2087. status, errType, code, message := concurrencyErrorResponse(err, "account")
  2088. writeError(status, errType, code, message)
  2089. return nil, openAISlotAcquireFailed
  2090. }
  2091. // Slot acquired: no longer waiting in queue.
  2092. releaseWait()
  2093. // 分组利润控制:WaitPlan 排队成功后终检。排队期间账号倍率
  2094. // 可能上调,越线则释放槽位交由调用方排除重选,不绑定粘连。
  2095. latest, vetoed, reason := h.gatewayService.ProfitControlVetoLatest(ctx, account)
  2096. if vetoed {
  2097. if accountReleaseFunc != nil {
  2098. accountReleaseFunc()
  2099. }
  2100. reqLog.Debug("openai.account_slot_profit_vetoed", zap.Int64("account_id", account.ID), zap.String("reason", reason))
  2101. return nil, openAISlotAcquireProfitVetoed
  2102. }
  2103. account = latest
  2104. selection.Account = latest
  2105. if err := h.gatewayService.BindStickySessionAfterProfitAdmission(ctx, groupID, sessionHash, account.ID); err != nil {
  2106. reqLog.Warn("openai.bind_sticky_session_after_profit_admission_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  2107. }
  2108. return wrapReleaseOnDone(ctx, accountReleaseFunc), openAISlotAcquireOK
  2109. }
  2110. // ResponsesWebSocket handles OpenAI Responses API WebSocket ingress endpoint
  2111. // GET /openai/v1/responses (Upgrade: websocket)
  2112. func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
  2113. if !isOpenAIWSUpgradeRequest(c.Request) {
  2114. h.errorResponse(c, http.StatusUpgradeRequired, "invalid_request_error", "WebSocket upgrade required (Upgrade: websocket)")
  2115. return
  2116. }
  2117. setOpenAIClientTransportWS(c)
  2118. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  2119. if !ok {
  2120. h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  2121. return
  2122. }
  2123. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  2124. if !ok {
  2125. h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  2126. return
  2127. }
  2128. reqLog := requestLogger(
  2129. c,
  2130. "handler.openai_gateway.responses_ws",
  2131. zap.Int64("user_id", subject.UserID),
  2132. zap.Int64("api_key_id", apiKey.ID),
  2133. zap.Any("group_id", apiKey.GroupID),
  2134. zap.Bool("openai_ws_mode", true),
  2135. )
  2136. if !h.ensureResponsesDependencies(c, reqLog) {
  2137. return
  2138. }
  2139. reqLog.Info("openai.websocket_ingress_started")
  2140. clientIP := ip.GetClientIP(c)
  2141. userAgent := strings.TrimSpace(c.GetHeader("User-Agent"))
  2142. clientLifecycleCtx := c.Request.Context()
  2143. ctx := clientLifecycleCtx
  2144. maxIngressConnections := 0
  2145. if h.cfg != nil {
  2146. maxIngressConnections = h.cfg.Gateway.OpenAIWS.MaxIngressConnectionsPerAPIKey
  2147. }
  2148. ingressLease, ingressLeaseAcquired, ingressLeaseErr := h.concurrencyHelper.AcquireOpenAIWSIngressLease(ctx, apiKey.ID, maxIngressConnections)
  2149. if ingressLeaseErr != nil {
  2150. reqLog.Error("openai.websocket_ingress_lease_acquire_failed", zap.Error(ingressLeaseErr))
  2151. h.errorResponse(c, http.StatusServiceUnavailable, "service_unavailable", "WebSocket ingress capacity is temporarily unavailable")
  2152. return
  2153. }
  2154. if !ingressLeaseAcquired {
  2155. reqLog.Info("openai.websocket_ingress_capacity_rejected", zap.Int("max_ingress_connections_per_api_key", maxIngressConnections))
  2156. c.Header("Retry-After", "5")
  2157. h.errorResponse(c, http.StatusTooManyRequests, "rate_limit_error", "Too many open WebSocket connections, please retry later")
  2158. return
  2159. }
  2160. if ingressLease != nil {
  2161. defer ingressLease.Release()
  2162. ctx = ingressLease.Context()
  2163. c.Request = c.Request.WithContext(ctx)
  2164. }
  2165. wsConn, err := coderws.Accept(c.Writer, c.Request, &coderws.AcceptOptions{
  2166. CompressionMode: coderws.CompressionContextTakeover,
  2167. })
  2168. if err != nil {
  2169. reqLog.Warn("openai.websocket_accept_failed",
  2170. zap.Error(err),
  2171. zap.String("client_ip", clientIP),
  2172. zap.String("request_user_agent", userAgent),
  2173. zap.String("upgrade_header", strings.TrimSpace(c.GetHeader("Upgrade"))),
  2174. zap.String("connection_header", strings.TrimSpace(c.GetHeader("Connection"))),
  2175. zap.String("sec_websocket_version", strings.TrimSpace(c.GetHeader("Sec-WebSocket-Version"))),
  2176. zap.Bool("has_sec_websocket_key", strings.TrimSpace(c.GetHeader("Sec-WebSocket-Key")) != ""),
  2177. )
  2178. return
  2179. }
  2180. defer func() {
  2181. _ = wsConn.CloseNow()
  2182. }()
  2183. wsConn.SetReadLimit(service.ResolveOpenAIWSClientReadLimitBytes(h.cfg))
  2184. firstMessageTimeout := service.ResolveOpenAIWSClientFirstMessageTimeout(h.cfg)
  2185. msgType, firstMessage, err := service.ReadOpenAIWSClientMessage(
  2186. ctx,
  2187. wsConn,
  2188. firstMessageTimeout,
  2189. coderws.StatusPolicyViolation,
  2190. "missing first response.create message",
  2191. )
  2192. if err != nil {
  2193. if errors.Is(context.Cause(ctx), service.ErrOpenAIWSIngressLeaseLost) {
  2194. reqLog.Warn("openai.websocket_ingress_lease_lost_before_first_message", zap.Error(err))
  2195. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "websocket ingress capacity lease lost; please reconnect")
  2196. return
  2197. }
  2198. closeStatus, closeReason := summarizeWSCloseErrorForLog(err)
  2199. reqLog.Warn("openai.websocket_read_first_message_failed",
  2200. zap.Error(err),
  2201. zap.String("client_ip", clientIP),
  2202. zap.String("close_status", closeStatus),
  2203. zap.String("close_reason", closeReason),
  2204. zap.Duration("read_timeout", firstMessageTimeout),
  2205. )
  2206. closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "missing first response.create message")
  2207. return
  2208. }
  2209. firstTurnStartedAt := time.Now()
  2210. if msgType != coderws.MessageText && msgType != coderws.MessageBinary {
  2211. closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "unsupported websocket message type")
  2212. return
  2213. }
  2214. if !gjson.ValidBytes(firstMessage) {
  2215. closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "invalid JSON payload")
  2216. return
  2217. }
  2218. reqModel := strings.TrimSpace(gjson.GetBytes(firstMessage, "model").String())
  2219. if reqModel == "" {
  2220. closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "model is required in first response.create payload")
  2221. return
  2222. }
  2223. // 分组级模型白名单:首帧校验客户端模型,不通过则关闭连接并标记运维原因。
  2224. // 必须在 ensureCompositeTargetPlatform(合成路由改写)之前执行。
  2225. // 与 HTTP 准入一致:帧内重复 model 键/大小写变体可能被上游按末值绑定,
  2226. // 全部候选值逐一校验,任一未命中即拒绝。
  2227. if blocked := blockedModelAllowlistCandidate(apiKey.Group, requestmodel.FromBodyCandidates("", "application/json", firstMessage)); blocked != "" {
  2228. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalModelConfiguration)
  2229. middleware2.MarkIngressRejected(c, middleware2.IngressRejectModelNotAllowed)
  2230. closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, fmt.Sprintf("Model %q is not available for this group", blocked))
  2231. return
  2232. }
  2233. ensureCompositeTargetPlatform(c, apiKey, reqModel)
  2234. ctx = c.Request.Context()
  2235. if apiKey.Group != nil && apiKey.Group.Platform == service.PlatformComposite {
  2236. platform, ok := service.ResolvedTargetPlatformFromContext(ctx)
  2237. if !ok || !isResponsesWebSocketCompositePlatform(platform) {
  2238. closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "Responses WebSocket API only supports OpenAI-compatible models for composite groups")
  2239. return
  2240. }
  2241. }
  2242. previousResponseID := strings.TrimSpace(gjson.GetBytes(firstMessage, "previous_response_id").String())
  2243. previousResponseIDKind := service.ClassifyOpenAIPreviousResponseIDKind(previousResponseID)
  2244. if previousResponseID != "" && previousResponseIDKind == service.OpenAIPreviousResponseIDKindMessageID {
  2245. closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "previous_response_id must be a response.id (resp_*), not a message id")
  2246. return
  2247. }
  2248. firstMessageToolCoverage := service.AnalyzeToolCallOutputContextCoverageBytes(firstMessage)
  2249. previousResponseCanMove := !firstMessageToolCoverage.HasFunctionCallOutput || firstMessageToolCoverage.ContextCoversAllCallIDs
  2250. reqLog = reqLog.With(
  2251. zap.Bool("ws_ingress", true),
  2252. zap.String("session_initial_model", reqModel),
  2253. zap.Bool("has_previous_response_id", previousResponseID != ""),
  2254. zap.String("previous_response_id_kind", previousResponseIDKind),
  2255. )
  2256. setOpsRequestContext(c, reqModel, true)
  2257. setOpsEndpointContext(c, "", int16(service.RequestTypeWSV2))
  2258. if decision := h.checkSecurityAuditStage(c, reqLog, apiKey, subject, service.ContentModerationProtocolOpenAIResponses, reqModel, firstMessage, "first_turn"); decision != nil && !decision.AllowNextStage {
  2259. writeSecurityAuditWSError(ctx, wsConn, decision)
  2260. closeOpenAIClientWS(wsConn, securityAuditWSCloseStatus(decision), securityAuditWSCloseReason(decision))
  2261. return
  2262. }
  2263. imageIntent := service.IsExplicitImageGenerationIntent("/v1/responses", reqModel, firstMessage)
  2264. if imageIntent && !service.GroupAllowsImageGeneration(apiKey.Group) {
  2265. closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, service.ImageGenerationPermissionMessage())
  2266. return
  2267. }
  2268. // The first response.create frame is available here, so explicit IDs are
  2269. // checked directly and body-derived sessions use the coarse scope gate.
  2270. if cyberBlockKey := findBlockedCyberSessionKey(c.Request.Context(), h.gatewayService, apiKey.ID, c, firstMessage); cyberBlockKey != "" {
  2271. writeCyberSessionBlockedWSError(c.Request.Context(), wsConn)
  2272. closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "session blocked by cyber-security policy")
  2273. h.enqueueCyberSessionBlockedOpsEntry(c, apiKey, reqModel, cyberBlockKey)
  2274. return
  2275. }
  2276. cyberBlockedThisConn := false
  2277. cyberBlockPendingAfterFailover := false
  2278. var cyberTurnBodiesMu sync.Mutex
  2279. cyberTurnBodies := map[int][]byte{1: append([]byte(nil), firstMessage...)}
  2280. setCyberTurnBody := func(turn int, payload []byte) {
  2281. cyberTurnBodiesMu.Lock()
  2282. cyberTurnBodies[turn] = append([]byte(nil), payload...)
  2283. cyberTurnBodiesMu.Unlock()
  2284. }
  2285. takeCyberTurnBody := func(turn int) []byte {
  2286. cyberTurnBodiesMu.Lock()
  2287. body := cyberTurnBodies[turn]
  2288. delete(cyberTurnBodies, turn)
  2289. cyberTurnBodiesMu.Unlock()
  2290. return body
  2291. }
  2292. // 解析渠道级模型映射
  2293. channelMappingWS, _ := h.gatewayService.ResolveChannelMappingAndRestrict(ctx, apiKey.GroupID, reqModel)
  2294. wsForwardModel := openAIChannelForwardModel(channelMappingWS, reqModel)
  2295. var currentUserRelease func()
  2296. var currentAccountRelease func()
  2297. releaseAccountSlot := func() {
  2298. if currentAccountRelease != nil {
  2299. currentAccountRelease()
  2300. currentAccountRelease = nil
  2301. }
  2302. }
  2303. releaseTurnSlots := func() {
  2304. releaseAccountSlot()
  2305. if currentUserRelease != nil {
  2306. currentUserRelease()
  2307. currentUserRelease = nil
  2308. }
  2309. }
  2310. // 必须尽早注册,确保任何 early return 都能释放已获取的并发槽位。
  2311. defer releaseTurnSlots()
  2312. userReleaseFunc, userAcquired, err := h.concurrencyHelper.TryAcquireUserSlotForAPIKey(ctx, subject.UserID, subject.Concurrency, apiKey.ID)
  2313. if err != nil {
  2314. reqLog.Warn("openai.websocket_user_slot_acquire_failed", zap.Error(err))
  2315. closeOpenAIClientWS(wsConn, coderws.StatusInternalError, "failed to acquire user concurrency slot")
  2316. return
  2317. }
  2318. if !userAcquired {
  2319. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "too many concurrent requests, please retry later")
  2320. return
  2321. }
  2322. currentUserRelease = wrapReleaseOnDone(ctx, userReleaseFunc)
  2323. ensureUserSlotHeld := func() bool {
  2324. if currentUserRelease != nil {
  2325. return true
  2326. }
  2327. userReleaseFunc, userAcquired, err := h.concurrencyHelper.TryAcquireUserSlotForAPIKey(ctx, subject.UserID, subject.Concurrency, apiKey.ID)
  2328. if err != nil {
  2329. reqLog.Warn("openai.websocket_user_slot_reacquire_failed", zap.Error(err))
  2330. closeOpenAIClientWS(wsConn, coderws.StatusInternalError, "failed to acquire user concurrency slot")
  2331. return false
  2332. }
  2333. if !userAcquired {
  2334. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "too many concurrent requests, please retry later")
  2335. return false
  2336. }
  2337. currentUserRelease = wrapReleaseOnDone(ctx, userReleaseFunc)
  2338. return true
  2339. }
  2340. subscription, _ := middleware2.GetSubscriptionFromContext(c)
  2341. requestPlatform := openAICompatibleRequestPlatform(ctx, apiKey)
  2342. requiredTransport := service.OpenAIUpstreamTransportResponsesWebsocketV2Ingress
  2343. if requestPlatform == service.PlatformGrok {
  2344. requiredTransport = service.OpenAIUpstreamTransportHTTPSSE
  2345. }
  2346. if err := h.billingCacheService.CheckBillingEligibility(ctx, apiKey.User, apiKey, apiKey.Group, subscription, service.QuotaPlatform(c.Request.Context(), apiKey)); err != nil {
  2347. reqLog.Info("openai.websocket_billing_eligibility_check_failed", zap.Error(err))
  2348. closeOpenAIClientWS(wsConn, coderws.StatusPolicyViolation, "billing check failed")
  2349. return
  2350. }
  2351. sessionHash := h.gatewayService.GenerateSessionHashWithFallback(
  2352. c,
  2353. firstMessage,
  2354. openAIWSIngressFallbackSessionSeed(subject.UserID, apiKey.ID, apiKey.GroupID),
  2355. )
  2356. ctx = service.WithOpenAIGuardianParentAffinity(ctx, c, firstMessage, reqModel)
  2357. maxAccountSwitches := h.maxAccountSwitches
  2358. switchCount := 0
  2359. profitVetoCount := 0
  2360. failedAccountIDs := make(map[int64]struct{})
  2361. sameAccountRetryCount := make(map[int64]int)
  2362. var lastFailoverErr *service.UpstreamFailoverError
  2363. var oauth429FailoverState service.OpenAIOAuth429FailoverState
  2364. wsAttemptMessage := append([]byte(nil), firstMessage...)
  2365. waitForWSSameAccountRetry := func(account *service.Account, failoverErr *service.UpstreamFailoverError) bool {
  2366. if account == nil || failoverErr == nil || failoverErr.StatusCode != http.StatusTooManyRequests || failoverErr.SameAccountRetryDeadline.IsZero() {
  2367. return false
  2368. }
  2369. retryLimit := effectiveSameAccountRetryLimit(failoverErr, account)
  2370. if !sameAccountRetryAllowed(failoverErr, sameAccountRetryCount[account.ID], retryLimit) {
  2371. return false
  2372. }
  2373. sameAccountRetryCount[account.ID]++
  2374. retryDelay := sameAccountRetryDelayFor(failoverErr, sameAccountRetryCount[account.ID])
  2375. reqLog.Warn("openai.websocket.same_account_retry",
  2376. zap.Int64("account_id", account.ID),
  2377. zap.Int("upstream_status", failoverErr.StatusCode),
  2378. zap.Int("retry_count", sameAccountRetryCount[account.ID]),
  2379. zap.Duration("retry_delay", retryDelay),
  2380. )
  2381. select {
  2382. case <-ctx.Done():
  2383. return false
  2384. case <-time.After(retryDelay):
  2385. return true
  2386. }
  2387. }
  2388. handleWSFailover := func(account *service.Account, failoverErr *service.UpstreamFailoverError) bool {
  2389. if ctx.Err() != nil {
  2390. return false
  2391. }
  2392. if failoverErr.ShouldReportAccountScheduleFailure() {
  2393. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, wsForwardModel, false, nil), false, nil, failoverErr)
  2394. }
  2395. releaseAccountSlot()
  2396. if !failoverErr.ShouldRetryNextAccount() {
  2397. closeOpenAIWSFailoverExhausted(c, wsConn, failoverErr)
  2398. return false
  2399. }
  2400. if ctx.Err() != nil {
  2401. return false
  2402. }
  2403. h.gatewayService.RecordOpenAIAccountSwitch()
  2404. failedAccountIDs[account.ID] = struct{}{}
  2405. lastFailoverErr = failoverErr
  2406. if switchCount >= maxAccountSwitches {
  2407. closeOpenAIWSFailoverExhausted(c, wsConn, failoverErr)
  2408. return false
  2409. }
  2410. switchCount++
  2411. if h.gatewayService.ShouldStopOpenAIOAuth429Failover(account, failoverErr.StatusCode, switchCount, &oauth429FailoverState) {
  2412. closeOpenAIWSFailoverExhausted(c, wsConn, failoverErr)
  2413. return false
  2414. }
  2415. reqLog.Warn("openai.websocket_upstream_failover_switching",
  2416. zap.Int64("account_id", account.ID),
  2417. zap.Int("upstream_status", failoverErr.StatusCode),
  2418. zap.Int("switch_count", switchCount),
  2419. zap.Int("max_switches", maxAccountSwitches),
  2420. )
  2421. if ctx.Err() != nil {
  2422. return false
  2423. }
  2424. return ensureUserSlotHeld()
  2425. }
  2426. // 与 HTTP Responses 路径保持一致:生图意图请求要求账号支持 Responses API(#4417)。
  2427. // WSv2 传输本身已隐含 Responses 支持,此处为防御性对齐。
  2428. // 使用 IsExplicitImageGenerationIntent 排除被动 namespace 声明(#4476)。
  2429. requiredCapability := service.OpenAIEndpointCapabilityChatCompletions
  2430. if service.IsExplicitImageGenerationIntent("/v1/responses", reqModel, firstMessage) && requestPlatform == service.PlatformOpenAI {
  2431. requiredCapability = service.OpenAIEndpointCapabilityResponses
  2432. }
  2433. // 分组利润控制:WS 桥按连接装配定价上下文并装门(选号与抢槽共用该
  2434. // ctx)。连接内不重选号,但每个 turn 开始经 BeforeTurn 重新冻结 pricingAt
  2435. // 并按最新门复核当前账号(准入与计费同源),峰前建连保活不能让后续 turn
  2436. // 继续按建连时刻的谷价计费。生图意图只影响能力路由与图片计费,不关门。
  2437. // 建连时刻只用于选号/准入,不作为任何 turn 的计费定价时刻。
  2438. wsPricingCtx, _ := h.gatewayService.WithOpenAIRequestPricingContext(ctx, apiKey.GroupID)
  2439. ctx = wsPricingCtx
  2440. for {
  2441. if ctx.Err() != nil {
  2442. return
  2443. }
  2444. reqLog.Debug("openai.websocket_account_selecting", zap.Int("excluded_account_count", len(failedAccountIDs)))
  2445. selection, scheduleDecision, err := h.gatewayService.SelectAccountWithSchedulerForCapability(
  2446. ctx,
  2447. apiKey.GroupID,
  2448. previousResponseID,
  2449. sessionHash,
  2450. wsForwardModel,
  2451. failedAccountIDs,
  2452. requiredTransport,
  2453. requiredCapability,
  2454. false,
  2455. previousResponseCanMove,
  2456. !imageIntent,
  2457. requestPlatform,
  2458. )
  2459. if err != nil {
  2460. reqLog.Warn("openai.websocket_account_select_failed",
  2461. zap.Error(openAICompatibleSelectionErrorForLog(err, requestPlatform)),
  2462. zap.Int("excluded_account_count", len(failedAccountIDs)),
  2463. )
  2464. if lastFailoverErr != nil {
  2465. closeOpenAIWSFailoverExhausted(c, wsConn, lastFailoverErr)
  2466. } else {
  2467. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "no available account")
  2468. }
  2469. return
  2470. }
  2471. if selection == nil || selection.Account == nil {
  2472. if lastFailoverErr != nil {
  2473. closeOpenAIWSFailoverExhausted(c, wsConn, lastFailoverErr)
  2474. } else {
  2475. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "no available account")
  2476. }
  2477. return
  2478. }
  2479. account := selection.Account
  2480. accountMaxConcurrency := account.Mode1EffectiveConcurrency()
  2481. if selection.WaitPlan != nil && selection.WaitPlan.MaxConcurrency > 0 {
  2482. accountMaxConcurrency = selection.WaitPlan.MaxConcurrency
  2483. }
  2484. if account.IsMode1ProtectionEnabled() && (accountMaxConcurrency <= 0 || accountMaxConcurrency > account.Mode1EffectiveConcurrency()) {
  2485. accountMaxConcurrency = account.Mode1EffectiveConcurrency()
  2486. }
  2487. // 终检、准入后绑定与后续 turn 级复核都使用选号结果携带的门(composite
  2488. // 等跨分组调度的门只存在于调度栈局部 ctx);准入成功后并入连接 ctx。
  2489. admissionCtx := service.ContextWithSelectionProfitGate(ctx, selection)
  2490. accountReleaseFunc := selection.ReleaseFunc
  2491. if selection.Acquired {
  2492. // 调度器已抢槽路径同样终检:选号与抢槽之间账号倍率可能刷新。
  2493. latest, vetoed, reason := h.gatewayService.ProfitControlVetoLatest(admissionCtx, account)
  2494. if vetoed {
  2495. if accountReleaseFunc != nil {
  2496. accountReleaseFunc()
  2497. }
  2498. reqLog.Debug("openai.websocket_account_slot_profit_vetoed", zap.Int64("account_id", account.ID), zap.String("reason", reason))
  2499. if !recordOpenAIProfitVeto(failedAccountIDs, account.ID, &profitVetoCount) {
  2500. reqLog.Warn("openai.websocket_profit_veto_attempts_exhausted", zap.Int("profit_veto_count", profitVetoCount))
  2501. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "no available account")
  2502. return
  2503. }
  2504. continue
  2505. }
  2506. account = latest
  2507. selection.Account = latest
  2508. }
  2509. if !selection.Acquired {
  2510. if selection.WaitPlan == nil {
  2511. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "account is busy, please retry later")
  2512. return
  2513. }
  2514. fastReleaseFunc, fastAcquired, err := h.concurrencyHelper.TryAcquireAccountSlot(
  2515. ctx,
  2516. account.ID,
  2517. selection.WaitPlan.MaxConcurrency,
  2518. )
  2519. if err != nil {
  2520. reqLog.Warn("openai.websocket_account_slot_acquire_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  2521. closeOpenAIClientWS(wsConn, coderws.StatusInternalError, "failed to acquire account concurrency slot")
  2522. return
  2523. }
  2524. if !fastAcquired {
  2525. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "account is busy, please retry later")
  2526. return
  2527. }
  2528. // 分组利润控制:WS 快速抢槽成功后终检,越线则释放
  2529. // 槽位、排除该账号重新选号,全池耗尽由下一轮选号关闭连接。
  2530. latest, vetoed, reason := h.gatewayService.ProfitControlVetoLatest(admissionCtx, account)
  2531. if vetoed {
  2532. if fastReleaseFunc != nil {
  2533. fastReleaseFunc()
  2534. }
  2535. reqLog.Debug("openai.websocket_account_slot_profit_vetoed", zap.Int64("account_id", account.ID), zap.String("reason", reason))
  2536. if !recordOpenAIProfitVeto(failedAccountIDs, account.ID, &profitVetoCount) {
  2537. reqLog.Warn("openai.websocket_profit_veto_attempts_exhausted", zap.Int("profit_veto_count", profitVetoCount))
  2538. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "no available account")
  2539. return
  2540. }
  2541. continue
  2542. }
  2543. account = latest
  2544. selection.Account = latest
  2545. accountReleaseFunc = fastReleaseFunc
  2546. }
  2547. // 准入完成:门并入连接 ctx,turn 级复核与 failover 重选共用。
  2548. ctx = admissionCtx
  2549. // Account selection starts a fresh upstream attempt. Clear any model
  2550. // captured by the previous failover account before credential lookup.
  2551. setOpsSelectedAccount(c, account.ID, account.Platform)
  2552. currentAccountRelease = wrapReleaseOnDone(ctx, accountReleaseFunc)
  2553. if err := h.gatewayService.BindStickySessionAfterProfitAdmission(ctx, apiKey.GroupID, sessionHash, account.ID); err != nil {
  2554. reqLog.Warn("openai.websocket_bind_sticky_session_after_profit_admission_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  2555. }
  2556. token, _, err := h.gatewayService.GetRequestCredential(ctx, c, account)
  2557. if err != nil {
  2558. reqLog.Warn("openai.websocket_get_access_token_failed", zap.Int64("account_id", account.ID), zap.Error(err))
  2559. if ctx.Err() != nil {
  2560. return
  2561. }
  2562. var failoverErr *service.UpstreamFailoverError
  2563. if errors.As(err, &failoverErr) {
  2564. if handleWSFailover(account, failoverErr) {
  2565. continue
  2566. }
  2567. return
  2568. }
  2569. closeOpenAIClientWS(wsConn, coderws.StatusInternalError, "failed to get access token")
  2570. return
  2571. }
  2572. reqLog.Debug("openai.websocket_account_selected",
  2573. zap.Int64("account_id", account.ID),
  2574. zap.String("account_name", account.Name),
  2575. zap.String("schedule_layer", scheduleDecision.Layer),
  2576. zap.Int("candidate_count", scheduleDecision.CandidateCount),
  2577. )
  2578. maxReasoningEffort, reasoningEffortMappings, maxReasoningEffortOverLimit, _ := openAIReasoningEffortPolicyForRequest(c, apiKey)
  2579. var requestPayloadHash string
  2580. var turnStartsMu sync.Mutex
  2581. turnStarts := make(map[int]time.Time, 4)
  2582. recordTurnStart := func(turn int, startedAt time.Time) {
  2583. if turn <= 0 || startedAt.IsZero() {
  2584. return
  2585. }
  2586. turnStartsMu.Lock()
  2587. turnStarts[turn] = startedAt
  2588. turnStartsMu.Unlock()
  2589. }
  2590. getTurnStart := func(turn int) time.Time {
  2591. turnStartsMu.Lock()
  2592. startedAt := turnStarts[turn]
  2593. delete(turnStarts, turn)
  2594. turnStartsMu.Unlock()
  2595. return startedAt
  2596. }
  2597. // Passthrough rejects overlapping response.create frames, so one immutable
  2598. // turn-tagged slot preserves the exact mapping used for the in-flight request.
  2599. var turnChannelMapping atomic.Pointer[openAIWSTurnChannelMappingSnapshot]
  2600. turnChannelMapping.Store(&openAIWSTurnChannelMappingSnapshot{turn: 1, mapping: channelMappingWS})
  2601. // turn 级定价:首轮回退到 TurnStarted 的所属 turn 时刻;后续 turn 由
  2602. // BeforeTurn 重新冻结 pricingAt 并按最新门复核当前账号。
  2603. var turnPricing openAIWSTurnPricing
  2604. hooks := &service.OpenAIWSIngressHooks{
  2605. ClientLifecycleContext: clientLifecycleCtx,
  2606. InitialRequestModel: reqModel,
  2607. InitialTurnStartedAt: firstTurnStartedAt,
  2608. MaxReasoningEffort: maxReasoningEffort,
  2609. MaxReasoningEffortOverLimit: maxReasoningEffortOverLimit,
  2610. ReasoningEffortMappings: reasoningEffortMappings,
  2611. TurnStarted: recordTurnStart,
  2612. BeforeRequest: func(turn int, payload []byte, originalModel string) error {
  2613. c.Set(securityAuditWSTurnContextKey, turn)
  2614. service.BeginOpsStreamTurn(c, turn)
  2615. setCyberTurnBody(turn, payload)
  2616. // 连接级 cyber session gate 也在 BeforeRequest 先执行,使 native 与
  2617. // passthrough ingress 都能在 BeforeTurn 及上游写入前无副作用地拒绝。
  2618. // BeforeTurn 中保留同一检查作为防御式兜底。
  2619. if cyberBlockedThisConn {
  2620. return service.NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, cyberSessionBlockedClientMsg, nil)
  2621. }
  2622. if turn == 1 {
  2623. return nil
  2624. }
  2625. if !gjson.ValidBytes(payload) {
  2626. return service.NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", errors.New("invalid json"))
  2627. }
  2628. model := strings.TrimSpace(originalModel)
  2629. if model == "" {
  2630. model = strings.TrimSpace(gjson.GetBytes(payload, "model").String())
  2631. }
  2632. if model == "" {
  2633. model = reqModel
  2634. }
  2635. // 分组级模型白名单:后续 turn 同样校验客户端模型(省略 model 时
  2636. // 沿用会话实际生效模型,含 session.update 轮换后的模型),不通过
  2637. // 则关闭整条连接,与推理强度 deny 一致。实际生效模型始终参与校验;
  2638. // 帧内重复 model 键/大小写变体/嵌套 session.model 额外逐一校验,
  2639. // 防止候选集非空时掩盖被轮换掉的禁用模型。
  2640. candidates := append([]string{model}, requestmodel.FromBodyCandidates("", "application/json", payload)...)
  2641. if blocked := blockedModelAllowlistCandidate(apiKey.Group, candidates); blocked != "" {
  2642. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalModelConfiguration)
  2643. middleware2.MarkIngressRejected(c, middleware2.IngressRejectModelNotAllowed)
  2644. return service.NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, fmt.Sprintf("Model %q is not available for this group", blocked), nil)
  2645. }
  2646. if decision := h.checkSecurityAuditStage(c, reqLog, apiKey, subject, service.ContentModerationProtocolOpenAIResponses, model, payload, "subsequent_turn"); decision != nil && !decision.AllowNextStage {
  2647. writeSecurityAuditWSError(ctx, wsConn, decision)
  2648. return service.NewOpenAIWSClientCloseError(securityAuditWSCloseStatus(decision), securityAuditWSCloseReason(decision), nil)
  2649. }
  2650. return nil
  2651. },
  2652. MapRequestModel: func(turn int, originalModel string) (string, error) {
  2653. model := strings.TrimSpace(originalModel)
  2654. if model == "" {
  2655. model = reqModel
  2656. }
  2657. setOpsRequestContext(c, model, true)
  2658. mapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(ctx, apiKey.GroupID, model)
  2659. mappedModelUnchanged := false
  2660. if previous := turnChannelMapping.Load(); previous != nil && previous.turn < turn {
  2661. mappedModelUnchanged = strings.TrimSpace(previous.mapping.MappedModel) == strings.TrimSpace(mapping.MappedModel)
  2662. }
  2663. if turn > 1 && !mappedModelUnchanged && !account.IsModelSupported(model) && !account.IsModelSupported(mapping.MappedModel) {
  2664. return "", newOpenAIWSUnsupportedModelSwitchError(mapping.MappedModel)
  2665. }
  2666. turnChannelMapping.Store(&openAIWSTurnChannelMappingSnapshot{turn: turn, mapping: mapping})
  2667. return mapping.MappedModel, nil
  2668. },
  2669. BeforeTurn: func(turn int) error {
  2670. // turn==1 的会话屏蔽已由握手层检查覆盖;连接内 flag 只拦截后续 turn。
  2671. if cyberBlockedThisConn {
  2672. return service.NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, cyberSessionBlockedClientMsg, nil)
  2673. }
  2674. // 长连接跨峰谷/倍率刷新防护:每个 turn 按当前时刻重装门并复核
  2675. // 当前账号,越线即要求客户端重连重选(连接绑定单一上游账号,
  2676. // 无法中途换号)。本 turn 的准入与计费共用同一 pricingAt。
  2677. turnCtx, turnAt := h.gatewayService.WithOpenAITurnPricingContext(ctx, apiKey.GroupID)
  2678. if _, vetoed, reason := h.gatewayService.ProfitControlVetoLatest(turnCtx, account); vetoed {
  2679. reqLog.Info("openai.websocket_turn_profit_vetoed",
  2680. zap.Int("turn", turn),
  2681. zap.Int64("account_id", account.ID),
  2682. zap.String("reason", reason))
  2683. return service.NewOpenAIWSClientCloseError(coderws.StatusTryAgainLater, "account is no longer eligible for this connection, please reconnect", nil)
  2684. }
  2685. turnPricing.freeze(turnAt)
  2686. if turn == 1 {
  2687. return nil
  2688. }
  2689. // 防御式清理:避免异常路径下旧槽位覆盖导致泄漏。
  2690. releaseTurnSlots()
  2691. // 非首轮 turn 需要重新抢占并发槽位,避免长连接空闲占槽。
  2692. userReleaseFunc, userAcquired, err := h.concurrencyHelper.TryAcquireUserSlotForAPIKey(ctx, subject.UserID, subject.Concurrency, apiKey.ID)
  2693. if err != nil {
  2694. return service.NewOpenAIWSClientCloseError(coderws.StatusInternalError, "failed to acquire user concurrency slot", err)
  2695. }
  2696. if !userAcquired {
  2697. return service.NewOpenAIWSClientCloseError(coderws.StatusTryAgainLater, "too many concurrent requests, please retry later", nil)
  2698. }
  2699. accountReleaseFunc, accountAcquired, err := h.concurrencyHelper.TryAcquireAccountSlot(ctx, account.ID, accountMaxConcurrency)
  2700. if err != nil {
  2701. if userReleaseFunc != nil {
  2702. userReleaseFunc()
  2703. }
  2704. return service.NewOpenAIWSClientCloseError(coderws.StatusInternalError, "failed to acquire account concurrency slot", err)
  2705. }
  2706. if !accountAcquired {
  2707. if userReleaseFunc != nil {
  2708. userReleaseFunc()
  2709. }
  2710. return service.NewOpenAIWSClientCloseError(coderws.StatusTryAgainLater, "account is busy, please retry later", nil)
  2711. }
  2712. currentUserRelease = wrapReleaseOnDone(ctx, userReleaseFunc)
  2713. currentAccountRelease = wrapReleaseOnDone(ctx, accountReleaseFunc)
  2714. return nil
  2715. },
  2716. AfterTurn: func(turn int, result *service.OpenAIForwardResult, turnErr error) {
  2717. turnStart := getTurnStart(turn)
  2718. cyberBlockBody := takeCyberTurnBody(turn)
  2719. // 每次 attempt 都清 cyber mark;failover 链结束前保留 recorded guard,
  2720. // 避免同一逻辑 turn 换号后重复落风控。CyberBlocked 必须在 submit 前
  2721. // 同步预捕获(task 闭包由 worker 池异步执行,届时 mark 已清除)。
  2722. defer func() {
  2723. clearCyberPolicyAttemptState(c, !cyberBlockPendingAfterFailover)
  2724. }()
  2725. releaseTurnSlots()
  2726. turnRequestedModel := reqModel
  2727. turnUpstreamModel := ""
  2728. if result != nil && turn > 1 {
  2729. if model := strings.TrimSpace(result.Model); model != "" {
  2730. turnRequestedModel = model
  2731. }
  2732. }
  2733. if result != nil {
  2734. turnUpstreamModel = strings.TrimSpace(result.UpstreamModel)
  2735. }
  2736. var turnMapping service.ChannelMappingResult
  2737. if snapshot := turnChannelMapping.Load(); snapshot != nil && snapshot.turn == turn {
  2738. turnMapping = snapshot.mapping
  2739. } else {
  2740. turnMapping, _ = h.gatewayService.ResolveChannelMappingAndRestrict(ctx, apiKey.GroupID, turnRequestedModel)
  2741. }
  2742. if turnUpstreamModel == "" {
  2743. turnUpstreamModel = turnRequestedModel
  2744. }
  2745. turnUsageFields := turnMapping.ToUsageFields(turnRequestedModel, turnUpstreamModel)
  2746. cyberMarked := service.GetOpsCyberPolicy(c) != nil
  2747. h.recordCyberPolicyIfMarked(c, apiKey, account, subscription, turnRequestedModel, turnErr != nil, cyberBlockBody, turnUsageFields, requestPayloadHash)
  2748. cyberBlockedThisConn, cyberBlockPendingAfterFailover = advanceOpenAIWSCyberBlockState(
  2749. cyberBlockedThisConn,
  2750. cyberBlockPendingAfterFailover,
  2751. cyberMarked,
  2752. turnErr,
  2753. )
  2754. if turnErr != nil {
  2755. if result == nil || result.ImageCount <= 0 {
  2756. return
  2757. }
  2758. // cyber 命中时该 turn 的用量已由 recordCyberPolicyIfMarked(forwardErrored=true)
  2759. // 按真实 token 记录,这里不再走下方 RecordUsage,避免对同一 turn 双写/双扣费。
  2760. if service.GetOpsCyberPolicy(c) != nil {
  2761. return
  2762. }
  2763. reqLog.Warn("openai.websocket_partial_error_with_image_result",
  2764. zap.Int64("account_id", account.ID),
  2765. zap.Int("image_count", result.ImageCount),
  2766. zap.Error(turnErr),
  2767. )
  2768. }
  2769. if result == nil {
  2770. return
  2771. }
  2772. result.BillingModel = openAIWSTurnBillingModel(result, turnMapping, turnRequestedModel, turnUpstreamModel)
  2773. reqLog.Debug("openai.websocket_turn_billing",
  2774. zap.Int("turn", turn),
  2775. zap.String("turn_requested_model", turnRequestedModel),
  2776. zap.String("turn_upstream_model", turnUpstreamModel),
  2777. zap.String("billing_model", result.BillingModel),
  2778. )
  2779. // 排除 spark 影子:其 codex_* 仅由 QueryUsage(/wham/usage bengalfox)更新(外审第7轮 P1)。
  2780. if account.Type == service.AccountTypeOAuth && !account.IsShadow() {
  2781. h.gatewayService.UpdateCodexUsageSnapshotFromHeaders(ctx, account.ID, result.ResponseHeaders)
  2782. }
  2783. scheduleModel := turnUpstreamModel
  2784. if scheduleModel == "" {
  2785. scheduleModel = turnRequestedModel
  2786. }
  2787. h.gatewayService.ReportOpenAIAccountScheduleResult(account, scheduleModel, openAIForwardSucceededForScheduling(result), result.FirstTokenMs)
  2788. inboundEndpoint := GetInboundEndpoint(c)
  2789. upstreamEndpoint := resolveOpenAIUpstreamEndpoint(c, account, result)
  2790. quotaPlatform := service.QuotaPlatform(c.Request.Context(), apiKey)
  2791. sessionID := service.ExtractClientSessionID(c)
  2792. turnRecordPricingAt := turnPricing.currentOr(turnStart)
  2793. cyberBlocked := service.GetOpsCyberPolicy(c) != nil
  2794. h.submitOpenAIUsageRecordTask(ctx, result, func(taskCtx context.Context) {
  2795. if err := h.gatewayService.RecordUsage(taskCtx, &service.OpenAIRecordUsageInput{
  2796. Result: result,
  2797. APIKey: apiKey,
  2798. User: apiKey.User,
  2799. Account: account,
  2800. Subscription: subscription,
  2801. InboundEndpoint: inboundEndpoint,
  2802. UpstreamEndpoint: upstreamEndpoint,
  2803. UserAgent: userAgent,
  2804. IPAddress: clientIP,
  2805. RequestPayloadHash: requestPayloadHash,
  2806. APIKeyService: h.apiKeyService,
  2807. QuotaPlatform: quotaPlatform,
  2808. SessionID: sessionID,
  2809. ChannelUsageFields: turnUsageFields,
  2810. PricingAt: turnRecordPricingAt,
  2811. CyberBlocked: cyberBlocked,
  2812. }); err != nil {
  2813. reqLog.Error("openai.websocket_record_usage_failed",
  2814. zap.Int64("account_id", account.ID),
  2815. zap.String("request_id", result.RequestID),
  2816. zap.Error(err),
  2817. )
  2818. }
  2819. })
  2820. },
  2821. }
  2822. wsFirstMessage := wsAttemptMessage
  2823. // 切组/会话失配防护:previous_response_id 未在当前分组命中粘连账号(StickyPreviousHit=false),
  2824. // 说明该会话链不属于本次调度到的账号,原样转发会触发上游会话链鉴权失败(“鉴权失败,请检查 API Key”)。
  2825. // 故剥离首包里的 previous_response_id,改用首包内 input 重建上下文;带 function_call_output 的
  2826. // 工具续链无法重建,保持原样。仅作用于首轮首包,后续 turn 的续链由 WS 转发层既有逻辑处理。
  2827. if previousResponseID != "" && !scheduleDecision.StickyPreviousHit && previousResponseCanMove {
  2828. wsFirstMessage = service.RemovePreviousResponseIDFromBody(wsFirstMessage)
  2829. reqLog.Debug("openai.websocket_previous_response_id_stripped_cross_group",
  2830. zap.Int64("account_id", account.ID),
  2831. zap.String("schedule_layer", scheduleDecision.Layer),
  2832. )
  2833. }
  2834. // WebSocket 首包可能很大,hash 必须在 hooks 外算成字符串,避免 AfterTurn 闭包保活请求体。
  2835. requestPayloadHash = service.HashUsageRequestPayload(wsFirstMessage)
  2836. if preemptCtx, cleanupPreempt, armed := h.gatewayService.BeginOpenAIWSIngressSessionPreemption(ctx, c, account, wsFirstMessage); armed {
  2837. ctx = preemptCtx
  2838. defer cleanupPreempt()
  2839. }
  2840. for {
  2841. err := h.gatewayService.ProxyResponsesWebSocketFromClient(ctx, c, wsConn, account, token, wsFirstMessage, hooks)
  2842. if err == nil {
  2843. reqLog.Info("openai.websocket_ingress_closed", zap.Int64("account_id", account.ID))
  2844. return
  2845. }
  2846. if service.IsOpenAIWSSessionPreemptedError(err) {
  2847. return
  2848. }
  2849. var failoverErr *service.UpstreamFailoverError
  2850. if errors.As(err, &failoverErr) {
  2851. retryPayload, retryCurrentTurn := service.OpenAIWSCurrentTurnRetryPayload(err)
  2852. nextAttemptMessage, retrySafe := openAIWSNextAttemptMessage(wsAttemptMessage, retryPayload, retryCurrentTurn)
  2853. if !retrySafe {
  2854. closeOpenAIWSFailoverExhausted(c, wsConn, failoverErr)
  2855. return
  2856. }
  2857. wsAttemptMessage = nextAttemptMessage
  2858. if retryCurrentTurn {
  2859. previousResponseID = ""
  2860. reqLog.Warn("openai.websocket_current_turn_failover_retry",
  2861. zap.Int64("account_id", account.ID),
  2862. zap.Int("upstream_status", failoverErr.StatusCode),
  2863. zap.Int("retry_payload_bytes", len(retryPayload)),
  2864. )
  2865. }
  2866. if waitForWSSameAccountRetry(account, failoverErr) {
  2867. if failoverErr.ShouldReportAccountScheduleFailure() {
  2868. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, wsForwardModel, false, nil), false, nil, err)
  2869. }
  2870. if !ensureUserSlotHeld() {
  2871. return
  2872. }
  2873. if currentAccountRelease == nil {
  2874. accountRelease, acquired, acquireErr := h.concurrencyHelper.TryAcquireAccountSlot(ctx, account.ID, accountMaxConcurrency)
  2875. if acquireErr != nil || !acquired {
  2876. reqLog.Warn("openai.websocket_same_account_retry_slot_unavailable",
  2877. zap.Int64("account_id", account.ID),
  2878. zap.Error(acquireErr),
  2879. )
  2880. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "account is busy, please retry later")
  2881. return
  2882. }
  2883. currentAccountRelease = wrapReleaseOnDone(ctx, accountRelease)
  2884. }
  2885. wsFirstMessage = wsAttemptMessage
  2886. continue
  2887. }
  2888. if handleWSFailover(account, failoverErr) {
  2889. break
  2890. }
  2891. return
  2892. }
  2893. if errors.Is(context.Cause(ctx), service.ErrOpenAIWSIngressLeaseLost) {
  2894. reqLog.Warn("openai.websocket_ingress_lease_lost",
  2895. zap.Int64("account_id", account.ID),
  2896. zap.Error(err),
  2897. )
  2898. closeOpenAIClientWS(wsConn, coderws.StatusTryAgainLater, "websocket ingress capacity lease lost; please reconnect")
  2899. return
  2900. }
  2901. var closeErr *service.OpenAIWSClientCloseError
  2902. hasClientCloseErr := errors.As(err, &closeErr)
  2903. if openAIWSIngressEndedByClient(err) {
  2904. closedFields := []zap.Field{zap.Int64("account_id", account.ID)}
  2905. if hasClientCloseErr {
  2906. closedFields = append(closedFields, zap.String("reason", closeErr.Reason()))
  2907. } else {
  2908. closedFields = append(closedFields, zap.Error(err))
  2909. }
  2910. reqLog.Info("openai.websocket_ingress_closed_normally", closedFields...)
  2911. // A bare coderws.CloseError or a plain cancellation carries no
  2912. // gateway-chosen close frame; mirror the client's clean 1000
  2913. // rather than the 1011 the proxy-failure tail would have sent.
  2914. if hasClientCloseErr {
  2915. closeOpenAIClientWS(wsConn, closeErr.StatusCode(), closeErr.Reason())
  2916. } else {
  2917. closeOpenAIClientWS(wsConn, coderws.StatusNormalClosure, "")
  2918. }
  2919. return
  2920. }
  2921. if shouldReportOpenAIWSProxyAccountFailure(err) {
  2922. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, wsForwardModel, false, nil), false, nil, err)
  2923. }
  2924. closeStatus, closeReason := summarizeWSCloseErrorForLog(err)
  2925. proxyFailedFields := []zap.Field{
  2926. zap.Int64("account_id", account.ID),
  2927. zap.Error(err),
  2928. zap.String("close_status", closeStatus),
  2929. zap.String("close_reason", closeReason),
  2930. }
  2931. if account.Proxy != nil {
  2932. proxyFailedFields = append(proxyFailedFields,
  2933. zap.Int64("proxy_id", account.Proxy.ID),
  2934. zap.String("proxy_name", account.Proxy.Name),
  2935. zap.String("proxy_host", account.Proxy.Host),
  2936. zap.Int("proxy_port", account.Proxy.Port),
  2937. )
  2938. } else if account.ProxyID != nil {
  2939. proxyFailedFields = append(proxyFailedFields, zap.Int64p("proxy_id", account.ProxyID))
  2940. }
  2941. reqLog.Warn("openai.websocket_proxy_failed", proxyFailedFields...)
  2942. if hasClientCloseErr {
  2943. closeOpenAIClientWS(wsConn, closeErr.StatusCode(), closeErr.Reason())
  2944. return
  2945. }
  2946. closeOpenAIClientWS(wsConn, coderws.StatusInternalError, "upstream websocket proxy failed")
  2947. return
  2948. }
  2949. }
  2950. }
  2951. func (h *OpenAIGatewayHandler) recoverResponsesPanic(c *gin.Context, streamStarted *bool) {
  2952. recovered := recover()
  2953. if recovered == nil {
  2954. return
  2955. }
  2956. started := false
  2957. if streamStarted != nil {
  2958. started = *streamStarted
  2959. }
  2960. wroteFallback := h.ensureForwardErrorResponse(c, started)
  2961. requestLogger(c, "handler.openai_gateway.responses").Error(
  2962. "openai.responses_panic_recovered",
  2963. zap.Bool("fallback_error_response_written", wroteFallback),
  2964. zap.Any("panic", recovered),
  2965. zap.ByteString("stack", debug.Stack()),
  2966. )
  2967. }
  2968. // recoverAnthropicMessagesPanic recovers from panics in the Anthropic Messages
  2969. // handler and returns an Anthropic-formatted error response.
  2970. func (h *OpenAIGatewayHandler) recoverAnthropicMessagesPanic(c *gin.Context, streamStarted *bool) {
  2971. recovered := recover()
  2972. if recovered == nil {
  2973. return
  2974. }
  2975. started := streamStarted != nil && *streamStarted
  2976. requestLogger(c, "handler.openai_gateway.messages").Error(
  2977. "openai.messages_panic_recovered",
  2978. zap.Bool("stream_started", started),
  2979. zap.Any("panic", recovered),
  2980. zap.ByteString("stack", debug.Stack()),
  2981. )
  2982. if !started {
  2983. h.anthropicErrorResponse(c, http.StatusInternalServerError, "api_error", "Internal server error")
  2984. }
  2985. }
  2986. func (h *OpenAIGatewayHandler) ensureResponsesDependencies(c *gin.Context, reqLog *zap.Logger) bool {
  2987. missing := h.missingResponsesDependencies()
  2988. if len(missing) == 0 {
  2989. return true
  2990. }
  2991. if reqLog == nil {
  2992. reqLog = requestLogger(c, "handler.openai_gateway.responses")
  2993. }
  2994. reqLog.Error("openai.handler_dependencies_missing", zap.Strings("missing_dependencies", missing))
  2995. if c != nil && c.Writer != nil && !c.Writer.Written() {
  2996. c.JSON(http.StatusServiceUnavailable, gin.H{
  2997. "error": gin.H{
  2998. "type": "api_error",
  2999. "message": "Service temporarily unavailable",
  3000. },
  3001. })
  3002. }
  3003. return false
  3004. }
  3005. func (h *OpenAIGatewayHandler) missingResponsesDependencies() []string {
  3006. missing := make([]string, 0, 5)
  3007. if h == nil {
  3008. return append(missing, "handler")
  3009. }
  3010. if h.gatewayService == nil {
  3011. missing = append(missing, "gatewayService")
  3012. }
  3013. if h.billingCacheService == nil {
  3014. missing = append(missing, "billingCacheService")
  3015. }
  3016. if h.apiKeyService == nil {
  3017. missing = append(missing, "apiKeyService")
  3018. }
  3019. if h.concurrencyHelper == nil || h.concurrencyHelper.concurrencyService == nil {
  3020. missing = append(missing, "concurrencyHelper")
  3021. }
  3022. return missing
  3023. }
  3024. func getContextInt64(c *gin.Context, key string) (int64, bool) {
  3025. if c == nil || key == "" {
  3026. return 0, false
  3027. }
  3028. v, ok := c.Get(key)
  3029. if !ok {
  3030. return 0, false
  3031. }
  3032. switch t := v.(type) {
  3033. case int64:
  3034. return t, true
  3035. case int:
  3036. return int64(t), true
  3037. case int32:
  3038. return int64(t), true
  3039. case float64:
  3040. return int64(t), true
  3041. default:
  3042. return 0, false
  3043. }
  3044. }
  3045. func (h *OpenAIGatewayHandler) submitUsageRecordTask(parent context.Context, task service.UsageRecordTask) {
  3046. if task == nil {
  3047. return
  3048. }
  3049. task = wrapUsageRecordTaskContext(parent, task)
  3050. if h.usageRecordWorkerPool != nil {
  3051. if mode := h.usageRecordWorkerPool.Submit(task); mode != service.UsageRecordSubmitModeDroppedStopped {
  3052. return
  3053. }
  3054. // 池已停止(进程关停窗口):计费任务不能静默丢失,降级为内联同步执行。
  3055. // 显式配置的 drop/sample 溢出丢弃仍按配置语义保留。
  3056. logger.L().With(
  3057. zap.String("component", "handler.openai_gateway.responses"),
  3058. ).Warn("openai.usage_record_task_stopped_sync_fallback")
  3059. }
  3060. // 回退路径:worker 池未注入或已停止时同步执行,避免退回到无界 goroutine 模式。
  3061. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  3062. defer cancel()
  3063. defer func() {
  3064. if recovered := recover(); recovered != nil {
  3065. logger.L().With(
  3066. zap.String("component", "handler.openai_gateway.responses"),
  3067. zap.Any("panic", recovered),
  3068. ).Error("openai.usage_record_task_panic_recovered")
  3069. }
  3070. }()
  3071. task(ctx)
  3072. }
  3073. func (h *OpenAIGatewayHandler) submitOpenAIUsageRecordTask(parent context.Context, result *service.OpenAIForwardResult, task service.UsageRecordTask) {
  3074. // Money-critical bills never drop on pool overflow: media, search surcharge, voice.
  3075. if result != nil && (result.ImageCount > 0 || result.VideoCount > 0 ||
  3076. result.SearchCount > 0 || result.WebSearchCalls > 0 || result.AudioUsage != nil) {
  3077. h.submitMandatoryUsageRecordTask(parent, task)
  3078. return
  3079. }
  3080. h.submitUsageRecordTask(parent, task)
  3081. }
  3082. func (h *OpenAIGatewayHandler) submitMandatoryUsageRecordTask(parent context.Context, task service.UsageRecordTask) {
  3083. if task == nil {
  3084. return
  3085. }
  3086. task = wrapUsageRecordTaskContext(parent, task)
  3087. if h.usageRecordWorkerPool != nil {
  3088. if mode := h.usageRecordWorkerPool.Submit(task); !mode.Dropped() {
  3089. return
  3090. }
  3091. logger.L().With(
  3092. zap.String("component", "handler.openai_gateway.usage"),
  3093. ).Warn("openai.usage_record_task_mandatory_sync_fallback")
  3094. }
  3095. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  3096. defer cancel()
  3097. defer func() {
  3098. if recovered := recover(); recovered != nil {
  3099. logger.L().With(
  3100. zap.String("component", "handler.openai_gateway.usage"),
  3101. zap.Any("panic", recovered),
  3102. ).Error("openai.usage_record_task_panic_recovered")
  3103. }
  3104. }()
  3105. task(ctx)
  3106. }
  3107. func (h *OpenAIGatewayHandler) acquireImageGenerationSlot(c *gin.Context, streamStarted bool) (func(), bool) {
  3108. if h == nil || h.cfg == nil || h.imageLimiter == nil {
  3109. return nil, true
  3110. }
  3111. imageConcurrency := h.cfg.Gateway.ImageConcurrency
  3112. wait := strings.TrimSpace(imageConcurrency.OverflowMode) == config.ImageConcurrencyOverflowModeWait
  3113. release, acquired := h.imageLimiter.Acquire(
  3114. c.Request.Context(),
  3115. imageConcurrency.Enabled,
  3116. imageConcurrency.MaxConcurrentRequests,
  3117. wait,
  3118. time.Duration(imageConcurrency.WaitTimeoutSeconds)*time.Second,
  3119. imageConcurrency.MaxWaitingRequests,
  3120. )
  3121. if acquired {
  3122. return release, true
  3123. }
  3124. h.handleStreamingAwareErrorWithCode(c, http.StatusTooManyRequests, "rate_limit_error", gatewayConcurrencyLimitCode, "Image generation concurrency limit exceeded, please retry later", streamStarted, false)
  3125. return nil, false
  3126. }
  3127. // handleConcurrencyError handles concurrency-related acquire errors.
  3128. func (h *OpenAIGatewayHandler) handleConcurrencyError(c *gin.Context, err error, slotType string, streamStarted bool) {
  3129. status, errType, code, message := concurrencyErrorResponse(err, slotType)
  3130. h.handleStreamingAwareErrorWithCode(c, status, errType, code, message, streamStarted, false)
  3131. }
  3132. func (h *OpenAIGatewayHandler) handleFailoverExhausted(c *gin.Context, failoverErr *service.UpstreamFailoverError, streamStarted bool) {
  3133. if failoverErr == nil {
  3134. h.handleFailoverExhaustedSimple(c, http.StatusBadGateway, streamStarted)
  3135. return
  3136. }
  3137. if failoverErr.IsOpenAIRequestBodyTooLarge() {
  3138. service.SetOpsUpstreamError(c, http.StatusRequestEntityTooLarge, service.OpenAIRequestBodyTooLargeClientMessage, "")
  3139. h.handleStreamingAwareError(
  3140. c,
  3141. http.StatusRequestEntityTooLarge,
  3142. "invalid_request_error",
  3143. service.OpenAIRequestBodyTooLargeClientMessage,
  3144. streamStarted,
  3145. )
  3146. return
  3147. }
  3148. if failoverErr.Reason == service.OpenAIHTTPContinuationUnsupportedReason {
  3149. message := strings.TrimSpace(failoverErr.ClientMessage)
  3150. if message == "" {
  3151. message = "previous_response_id requires an OpenAI API-key account for HTTP requests"
  3152. }
  3153. h.handleStreamingAwareError(c, http.StatusBadRequest, "invalid_request_error", message, streamStarted)
  3154. return
  3155. }
  3156. copyFailoverRetryAfter(c, failoverErr.ResponseHeaders)
  3157. if failoverErr.Reason == "account_traffic_limit" {
  3158. h.handleStreamingAwareError(c, failoverErr.ClientStatusCode, service.AccountTrafficErrorType(failoverErr.ClientStatusCode), failoverErr.ClientMessage, streamStarted)
  3159. return
  3160. }
  3161. if failoverErr.IsCredentialFailure() {
  3162. status, message := credentialFailoverClientResponse(failoverErr)
  3163. h.handleStreamingAwareError(c, status, "upstream_error", message, streamStarted)
  3164. return
  3165. }
  3166. if failoverErr.IsOpenAICapacityShed() && strings.TrimSpace(failoverErr.ClientMessage) != "" {
  3167. status := failoverErr.ClientStatusCode
  3168. if status <= 0 {
  3169. status = http.StatusServiceUnavailable
  3170. }
  3171. h.handleStreamingAwareError(c, status, "server_error", failoverErr.ClientMessage, streamStarted)
  3172. return
  3173. }
  3174. statusCode := failoverErr.StatusCode
  3175. responseBody := failoverErr.ResponseBody
  3176. if statusCode == http.StatusBadRequest && service.IsOpenAICompatibleModelNotFound400(responseBody) && !streamStarted {
  3177. upstreamMsg := service.SanitizeUpstreamErrorMessage(service.ExtractUpstreamErrorMessage(responseBody))
  3178. service.SetOpsUpstreamError(c, statusCode, upstreamMsg, "")
  3179. service.WriteOpenAIUpstreamClientError(c, statusCode, responseBody, upstreamMsg)
  3180. return
  3181. }
  3182. if service.IsOpenAISilentRefusalErrorBody(responseBody) {
  3183. service.SetOpsUpstreamError(c, statusCode, service.OpenAISilentRefusalClientMessage(), "")
  3184. h.handleStreamingAwareError(c, http.StatusBadGateway, "upstream_error", service.OpenAISilentRefusalClientMessage(), streamStarted)
  3185. return
  3186. }
  3187. // 先检查透传规则
  3188. if h.errorPassthroughService != nil && len(responseBody) > 0 {
  3189. if rule := h.errorPassthroughService.MatchRule("openai", statusCode, responseBody); rule != nil {
  3190. // 确定响应状态码
  3191. respCode := statusCode
  3192. if !rule.PassthroughCode && rule.ResponseCode != nil {
  3193. respCode = *rule.ResponseCode
  3194. }
  3195. // 确定响应消息
  3196. msg := service.ExtractUpstreamErrorMessage(responseBody)
  3197. if !rule.PassthroughBody && rule.CustomMessage != nil {
  3198. msg = *rule.CustomMessage
  3199. }
  3200. if rule.SkipMonitoring {
  3201. c.Set(service.OpsSkipPassthroughKey, true)
  3202. }
  3203. h.handleStreamingAwareError(c, respCode, "upstream_error", msg, streamStarted)
  3204. return
  3205. }
  3206. }
  3207. // 记录原始上游状态码,以便 ops 错误日志捕获真实的上游错误
  3208. upstreamMsg := service.ExtractUpstreamErrorMessage(responseBody)
  3209. service.SetOpsUpstreamError(c, statusCode, upstreamMsg, "")
  3210. // 使用默认的错误映射
  3211. status, errType, errMsg := h.mapUpstreamError(statusCode)
  3212. h.handleStreamingAwareError(c, status, errType, errMsg, streamStarted)
  3213. }
  3214. func credentialFailoverClientResponse(failoverErr *service.UpstreamFailoverError) (int, string) {
  3215. if failoverErr != nil && failoverErr.Reason == service.OpenAIUpstreamAccessStateReason && strings.TrimSpace(failoverErr.ClientMessage) != "" {
  3216. status := failoverErr.ClientStatusCode
  3217. if status <= 0 {
  3218. status = http.StatusServiceUnavailable
  3219. }
  3220. return status, failoverErr.ClientMessage
  3221. }
  3222. if failoverErr != nil && failoverErr.Reason == service.AntigravityCredentialRejectedReason {
  3223. return http.StatusBadGateway, service.AntigravityCredentialRejectedClientMessage
  3224. }
  3225. return http.StatusServiceUnavailable, service.GrokCredentialUnavailableClientMessage
  3226. }
  3227. func copyFailoverRetryAfter(c *gin.Context, headers http.Header) {
  3228. if c == nil || headers == nil {
  3229. return
  3230. }
  3231. retryAfter := strings.TrimSpace(headers.Get("Retry-After"))
  3232. if retryAfter == "" || len(retryAfter) > 128 || strings.ContainsAny(retryAfter, "\r\n") || !isSafeRetryAfter(retryAfter) {
  3233. return
  3234. }
  3235. c.Header("Retry-After", retryAfter)
  3236. }
  3237. func isSafeRetryAfter(value string) bool {
  3238. digitsOnly := true
  3239. for _, char := range value {
  3240. if char < '0' || char > '9' {
  3241. digitsOnly = false
  3242. break
  3243. }
  3244. }
  3245. if digitsOnly {
  3246. seconds, err := strconv.ParseUint(value, 10, 32)
  3247. return err == nil && seconds <= uint64((7*24*time.Hour)/time.Second)
  3248. }
  3249. retryAt, err := http.ParseTime(value)
  3250. if err != nil {
  3251. return false
  3252. }
  3253. return !retryAt.After(time.Now().Add(7 * 24 * time.Hour))
  3254. }
  3255. // handleFailoverExhaustedSimple 简化版本,用于没有响应体的情况
  3256. func (h *OpenAIGatewayHandler) handleFailoverExhaustedSimple(c *gin.Context, statusCode int, streamStarted bool) {
  3257. status, errType, errMsg := h.mapUpstreamError(statusCode)
  3258. service.SetOpsUpstreamError(c, statusCode, errMsg, "")
  3259. h.handleStreamingAwareError(c, status, errType, errMsg, streamStarted)
  3260. }
  3261. func (h *OpenAIGatewayHandler) mapUpstreamError(statusCode int) (int, string, string) {
  3262. switch statusCode {
  3263. case 401:
  3264. return http.StatusBadGateway, "upstream_error", "Upstream authentication failed, please contact administrator"
  3265. case 403:
  3266. return http.StatusBadGateway, "upstream_error", "Upstream access forbidden, please contact administrator"
  3267. case 429:
  3268. return http.StatusTooManyRequests, "rate_limit_error", "Upstream rate limit exceeded, please retry later"
  3269. case 529:
  3270. return http.StatusServiceUnavailable, "upstream_error", "Upstream service overloaded, please retry later"
  3271. case 500, 502, 503, 504:
  3272. return http.StatusBadGateway, "upstream_error", "Upstream service temporarily unavailable"
  3273. default:
  3274. return http.StatusBadGateway, "upstream_error", "Upstream request failed"
  3275. }
  3276. }
  3277. // handleStreamingAwareError handles errors that may occur after streaming has started
  3278. func (h *OpenAIGatewayHandler) handleStreamingAwareError(c *gin.Context, status int, errType, message string, streamStarted bool) {
  3279. h.handleStreamingAwareErrorWithCode(c, status, errType, "", message, streamStarted, false)
  3280. }
  3281. func (h *OpenAIGatewayHandler) handleStreamingAwareErrorWithCode(
  3282. c *gin.Context,
  3283. status int,
  3284. errType string,
  3285. code string,
  3286. message string,
  3287. streamStarted bool,
  3288. countTowardsSLA bool,
  3289. ) {
  3290. // body-signal compact 心跳可能已把响应头提交为 200:先停心跳(建立
  3291. // happens-before,接管 ResponseWriter),并升级为流内错误处理。
  3292. if service.StopOpenAICompactSSEKeepaliveCommitted(c) {
  3293. streamStarted = true
  3294. }
  3295. if streamStarted {
  3296. if countTowardsSLA {
  3297. service.MarkOpsStreamFailure(c, errType, code, message, status)
  3298. } else {
  3299. service.MarkOpsStreamError(c, errType, message, status)
  3300. }
  3301. // /v1/responses 的严格 SDK(Codex CLI)要求终止事件必须属于
  3302. // response.completed/failed/incomplete/cancelled 集合。
  3303. // 通用 `event: error` 帧不被识别为终止事件,会导致
  3304. // "stream closed before response.completed"。
  3305. if inboundIsResponses(c) {
  3306. if writeResponsesFailedSSE(c, errType, code, message) {
  3307. return
  3308. }
  3309. }
  3310. // Stream already started, send error as SSE event then close
  3311. flusher, ok := c.Writer.(http.Flusher)
  3312. if ok {
  3313. errorObject := gin.H{"type": errType, "message": message}
  3314. if code != "" {
  3315. errorObject["code"] = code
  3316. }
  3317. payload, err := json.Marshal(gin.H{"error": errorObject})
  3318. if err != nil {
  3319. payload = []byte(`{"error":{"type":"upstream_error","message":"Upstream request failed"}}`)
  3320. }
  3321. errorEvent := "event: error\ndata: " + string(payload) + "\n\n"
  3322. if _, err := fmt.Fprint(c.Writer, errorEvent); err != nil {
  3323. _ = c.Error(err)
  3324. }
  3325. flusher.Flush()
  3326. }
  3327. return
  3328. }
  3329. // Normal case: return JSON response with proper status code
  3330. if code == "" {
  3331. h.errorResponse(c, status, errType, message)
  3332. return
  3333. }
  3334. c.JSON(status, gin.H{"error": gin.H{
  3335. "type": errType, "code": code, "message": message,
  3336. }})
  3337. }
  3338. func (h *OpenAIGatewayHandler) ensureOpenAIStreamReadErrorResponse(c *gin.Context, err error, streamStarted bool) bool {
  3339. code, message, ok := service.OpenAIUpstreamStreamReadErrorDetails(err)
  3340. if !ok || c == nil || c.Writer == nil || service.IsResponseCommitted(c) {
  3341. return false
  3342. }
  3343. if c.Writer.Written() {
  3344. streamStarted = true
  3345. }
  3346. h.handleStreamingAwareErrorWithCode(
  3347. c, http.StatusBadGateway, "upstream_error", code, message, streamStarted, true,
  3348. )
  3349. return true
  3350. }
  3351. // ensureForwardErrorResponse 在 Forward 返回错误但尚未写响应时补写统一错误响应。
  3352. func (h *OpenAIGatewayHandler) ensureForwardErrorResponse(c *gin.Context, streamStarted bool) bool {
  3353. if c == nil || c.Writer == nil {
  3354. return false
  3355. }
  3356. if c.Request != nil && errors.Is(c.Request.Context().Err(), context.Canceled) {
  3357. failoverClientGone(c)
  3358. return false
  3359. }
  3360. // 先停 compact 心跳再读 Writer 状态,避免与心跳 goroutine 竞争。
  3361. compactKeepaliveCommitted := service.StopOpenAICompactSSEKeepaliveCommitted(c)
  3362. if compactKeepaliveCommitted {
  3363. streamStarted = true
  3364. }
  3365. imageKeepalivePresent := service.OpenAIImagesJSONKeepalivePresent(c)
  3366. service.StopOpenAIImagesJSONKeepaliveCommitted(c)
  3367. imageKeepalivePaddingOnly := false
  3368. imageKeepaliveResponseWritten := false
  3369. if imageKeepalivePresent {
  3370. adjustedSize := service.OpenAIImagesJSONKeepaliveAdjustedWrittenSize(c)
  3371. imageKeepalivePaddingOnly = adjustedSize < 0
  3372. imageKeepaliveResponseWritten = adjustedSize >= 0
  3373. }
  3374. compactKeepaliveHasMeaningfulOutput := compactKeepaliveCommitted && service.OpenAICompactKeepaliveAdjustedWrittenSize(c) > 0
  3375. // Compact keepalive may have committed 200 headers without writing a
  3376. // semantic SSE event. In that case the Responses stream still needs its
  3377. // protocol-correct terminal response.failed event.
  3378. if (service.IsResponseCommitted(c) && (!compactKeepaliveCommitted || compactKeepaliveHasMeaningfulOutput)) || (!compactKeepaliveCommitted && imageKeepaliveResponseWritten) {
  3379. return false
  3380. }
  3381. if c.Writer.Written() && !imageKeepalivePaddingOnly {
  3382. streamStarted = true
  3383. }
  3384. h.handleStreamingAwareError(c, http.StatusBadGateway, "upstream_error", "Upstream request failed", streamStarted)
  3385. return true
  3386. }
  3387. func shouldLogOpenAIForwardFailureAsWarn(c *gin.Context, wroteFallback bool) bool {
  3388. if wroteFallback {
  3389. return false
  3390. }
  3391. if c == nil || c.Writer == nil {
  3392. return false
  3393. }
  3394. return c.Writer.Written()
  3395. }
  3396. // openAIForwardErrorAlreadyCommunicated reports whether Forward returned an
  3397. // error after it had already written the upstream terminal error response to
  3398. // the client.
  3399. //
  3400. // This matters for Responses streams: upstream may return HTTP 200 with a
  3401. // non-retryable `response.failed` event (for example a policy/safety rejection).
  3402. // The service layer forwards that terminal event verbatim, then returns an
  3403. // error so the caller can log/account for the failed upstream response. The
  3404. // handler must not append its generic fallback `response.failed`, otherwise
  3405. // strict clients may see the useful upstream message replaced by "Upstream
  3406. // request failed" or receive duplicate terminal events.
  3407. func openAIForwardErrorAlreadyCommunicated(c *gin.Context, writerSizeBeforeForward int, err error) bool {
  3408. if err == nil || c == nil || c.Writer == nil {
  3409. return false
  3410. }
  3411. // 与快照同口径:排除 compact 心跳字节,避免"仅心跳写出"被误判为
  3412. // 响应已写出(#3887)。
  3413. if service.OpenAICompactKeepaliveAdjustedWrittenSize(c) == writerSizeBeforeForward ||
  3414. service.OpenAIImagesJSONKeepaliveAdjustedWrittenSize(c) == writerSizeBeforeForward {
  3415. return false
  3416. }
  3417. // cyber_policy 命中时上游原始错误体已透传给客户端(非流式 c.Data 写出 400 body,
  3418. // 流式写出 response.failed 事件),不能再让 ensureForwardErrorResponse 追加
  3419. // fallback —— 否则在已写出的完整响应尾部追加 SSE(responses 端点尾随
  3420. // response.failed、chat 端点尾随 event:error),污染响应体。Size 已变化证明响应确已写出。
  3421. if service.GetOpsCyberPolicy(c) != nil {
  3422. return true
  3423. }
  3424. msg := strings.TrimSpace(err.Error())
  3425. for _, prefix := range []string{
  3426. "upstream response failed:",
  3427. "non-streaming openai protocol error:",
  3428. } {
  3429. if strings.HasPrefix(msg, prefix) {
  3430. return true
  3431. }
  3432. }
  3433. return false
  3434. }
  3435. func openAIForwardMayFailover(c *gin.Context, writerSizeBeforeForward int, failoverErr *service.UpstreamFailoverError) bool {
  3436. if c == nil || c.Writer == nil {
  3437. return false
  3438. }
  3439. if service.OpenAICompactKeepaliveAdjustedWrittenSize(c) == writerSizeBeforeForward {
  3440. return true
  3441. }
  3442. return failoverErr != nil && failoverErr.SafeToFailoverAfterWrite
  3443. }
  3444. func openAIRequestAllowsFailoverReplay(c *gin.Context) bool {
  3445. if c == nil || c.Request == nil {
  3446. return false
  3447. }
  3448. return !failoverClientGone(c)
  3449. }
  3450. func openAIFirstOutputFailoverExhausted(failoverErr *service.UpstreamFailoverError, switchCount *int) bool {
  3451. if failoverErr == nil || !failoverErr.SafeToFailoverAfterWrite || switchCount == nil {
  3452. return false
  3453. }
  3454. if *switchCount >= maxOpenAIFirstOutputTimeoutSwitches {
  3455. return true
  3456. }
  3457. *switchCount = *switchCount + 1
  3458. return false
  3459. }
  3460. // errorResponse returns OpenAI API format error response
  3461. func (h *OpenAIGatewayHandler) errorResponse(c *gin.Context, status int, errType, message string) {
  3462. // body-signal compact 心跳可能已把响应头提交为 200:JSON 错误体会与已
  3463. // 提交的 SSE 流交错,必须降级为 response.failed 终止事件(#3887)。
  3464. if service.StopOpenAICompactSSEKeepaliveCommitted(c) {
  3465. service.MarkOpsStreamError(c, errType, message, status)
  3466. if writeResponsesFailedSSE(c, errType, "", message) {
  3467. return
  3468. }
  3469. }
  3470. c.JSON(status, gin.H{
  3471. "error": gin.H{
  3472. "type": errType,
  3473. "message": message,
  3474. },
  3475. })
  3476. }
  3477. // openAICompactKeepaliveInterval 复用流式 keepalive 配置作为 compact 下游
  3478. // 心跳间隔;0 表示禁用(与流式路径语义一致)。
  3479. func (h *OpenAIGatewayHandler) openAICompactKeepaliveInterval() time.Duration {
  3480. if h.cfg == nil || h.cfg.Gateway.StreamKeepaliveInterval <= 0 {
  3481. return 0
  3482. }
  3483. return time.Duration(h.cfg.Gateway.StreamKeepaliveInterval) * time.Second
  3484. }
  3485. func setOpenAIClientTransportHTTP(c *gin.Context) {
  3486. service.SetOpenAIClientTransport(c, service.OpenAIClientTransportHTTP)
  3487. }
  3488. func setOpenAIClientTransportWS(c *gin.Context) {
  3489. service.SetOpenAIClientTransport(c, service.OpenAIClientTransportWS)
  3490. }
  3491. func ensureOpenAIPoolModeSessionHash(sessionHash string, account *service.Account) string {
  3492. if sessionHash != "" || account == nil || !account.IsPoolMode() {
  3493. return sessionHash
  3494. }
  3495. // 为当前请求生成一次性粘性会话键,确保同账号重试不会重新负载均衡到其他账号。
  3496. return "openai-pool-retry-" + uuid.NewString()
  3497. }
  3498. func openAIWSIngressFallbackSessionSeed(userID, apiKeyID int64, groupID *int64) string {
  3499. gid := int64(0)
  3500. if groupID != nil {
  3501. gid = *groupID
  3502. }
  3503. return fmt.Sprintf("openai_ws_ingress:%d:%d:%d", gid, userID, apiKeyID)
  3504. }
  3505. func isOpenAIWSUpgradeRequest(r *http.Request) bool {
  3506. if r == nil {
  3507. return false
  3508. }
  3509. if !strings.EqualFold(strings.TrimSpace(r.Header.Get("Upgrade")), "websocket") {
  3510. return false
  3511. }
  3512. return strings.Contains(strings.ToLower(strings.TrimSpace(r.Header.Get("Connection"))), "upgrade")
  3513. }
  3514. // blockedModelAllowlistCandidate 对全部候选模型逐一校验分组白名单,返回第一个
  3515. // 未命中的值(全部命中或白名单未开启返回空串)。WS 帧与 HTTP 请求体共用该
  3516. // 规则:重复 model 键/大小写变体可能被上游按末值绑定,任一未命中即拒绝。
  3517. func blockedModelAllowlistCandidate(group *service.Group, candidates []string) string {
  3518. if group == nil || !group.ModelAllowlistEnabled() {
  3519. return ""
  3520. }
  3521. for _, candidate := range candidates {
  3522. if !group.ModelAllowlist.Allows(candidate) {
  3523. return candidate
  3524. }
  3525. }
  3526. return ""
  3527. }
  3528. func closeOpenAIClientWS(conn *coderws.Conn, status coderws.StatusCode, reason string) {
  3529. if conn == nil {
  3530. return
  3531. }
  3532. reason = strings.TrimSpace(reason)
  3533. if len(reason) > 120 {
  3534. reason = reason[:120]
  3535. }
  3536. _ = conn.Close(status, reason)
  3537. _ = conn.CloseNow()
  3538. }
  3539. func openAIWSNextAttemptMessage(current, retryPayload []byte, retryCurrentTurn bool) ([]byte, bool) {
  3540. if !retryCurrentTurn {
  3541. return append([]byte(nil), current...), true
  3542. }
  3543. if len(retryPayload) == 0 {
  3544. return nil, false
  3545. }
  3546. return append([]byte(nil), retryPayload...), true
  3547. }
  3548. func closeOpenAIWSFailoverExhausted(c *gin.Context, conn *coderws.Conn, failoverErr *service.UpstreamFailoverError) {
  3549. intendedStatus := http.StatusBadGateway
  3550. errorType := "upstream_error"
  3551. errorCode := "upstream_ws_failover_exhausted"
  3552. message := "upstream websocket proxy failed"
  3553. closeStatus := coderws.StatusInternalError
  3554. if failoverErr != nil {
  3555. if reason := strings.TrimSpace(string(failoverErr.Reason)); reason != "" {
  3556. errorCode = reason
  3557. }
  3558. if failoverErr.Reason == "account_traffic_limit" {
  3559. intendedStatus = failoverErr.StatusCode
  3560. errorType = service.AccountTrafficErrorType(intendedStatus)
  3561. message = failoverErr.ClientMessage
  3562. closeStatus = coderws.StatusTryAgainLater
  3563. if conn != nil {
  3564. payload, _ := json.Marshal(gin.H{"type": "error", "error": gin.H{"type": errorType, "code": errorCode, "message": message, "retry_after": failoverErr.ResponseHeaders.Get("Retry-After")}})
  3565. writeCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
  3566. _ = conn.Write(writeCtx, coderws.MessageText, payload)
  3567. cancel()
  3568. }
  3569. } else if failoverErr.Stage == service.GatewayFailureStageAccountAuth {
  3570. intendedStatus = http.StatusServiceUnavailable
  3571. errorType = "api_error"
  3572. message = service.GrokCredentialUnavailableClientMessage
  3573. closeStatus = coderws.StatusTryAgainLater
  3574. } else {
  3575. switch failoverErr.StatusCode {
  3576. case http.StatusTooManyRequests:
  3577. intendedStatus = http.StatusTooManyRequests
  3578. errorType = "rate_limit_error"
  3579. message = "upstream rate limit exceeded, please retry later"
  3580. closeStatus = coderws.StatusTryAgainLater
  3581. case 529, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
  3582. intendedStatus = failoverErr.StatusCode
  3583. message = "upstream service temporarily unavailable"
  3584. closeStatus = coderws.StatusTryAgainLater
  3585. case http.StatusUnauthorized, http.StatusForbidden:
  3586. intendedStatus = failoverErr.StatusCode
  3587. errorType = "authentication_error"
  3588. message = "upstream websocket authentication failed"
  3589. closeStatus = coderws.StatusPolicyViolation
  3590. }
  3591. }
  3592. }
  3593. service.MarkOpsStreamFailure(c, errorType, errorCode, message, intendedStatus)
  3594. closeOpenAIClientWS(conn, closeStatus, message)
  3595. }
  3596. func writeContentModerationWSError(ctx context.Context, conn *coderws.Conn, decision *service.ContentModerationDecision) {
  3597. if conn == nil || decision == nil {
  3598. return
  3599. }
  3600. if ctx == nil {
  3601. ctx = context.Background()
  3602. }
  3603. message := strings.TrimSpace(decision.Message)
  3604. if message == "" {
  3605. message = "content moderation blocked this request"
  3606. }
  3607. payload, err := json.Marshal(gin.H{
  3608. "event_id": "evt_content_moderation_blocked",
  3609. "type": "error",
  3610. "error": gin.H{
  3611. "type": "invalid_request_error",
  3612. "code": contentModerationErrorCode(decision),
  3613. "message": message,
  3614. },
  3615. })
  3616. if err != nil {
  3617. payload = []byte(`{"event_id":"evt_content_moderation_blocked","type":"error","error":{"type":"invalid_request_error","code":"content_policy_violation","message":"content moderation blocked this request"}}`)
  3618. }
  3619. writeCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
  3620. defer cancel()
  3621. _ = conn.Write(writeCtx, coderws.MessageText, payload)
  3622. }
  3623. // writeCyberSessionBlockedWSError sends an error frame telling the client this
  3624. // session is blocked by the cyber session block (F5a) before closing.
  3625. func writeCyberSessionBlockedWSError(ctx context.Context, conn *coderws.Conn) {
  3626. if conn == nil {
  3627. return
  3628. }
  3629. if ctx == nil {
  3630. ctx = context.Background()
  3631. }
  3632. payload, err := json.Marshal(gin.H{
  3633. "event_id": "evt_cyber_session_blocked",
  3634. "type": "error",
  3635. "error": gin.H{
  3636. "type": "permission_error",
  3637. "code": "session_blocked_by_cyber_policy",
  3638. "message": cyberSessionBlockedClientMsg,
  3639. },
  3640. })
  3641. if err != nil {
  3642. payload = []byte(`{"event_id":"evt_cyber_session_blocked","type":"error","error":{"type":"permission_error","code":"session_blocked_by_cyber_policy","message":"This session is blocked by cyber-security policy, please start a new session"}}`)
  3643. }
  3644. writeCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
  3645. defer cancel()
  3646. _ = conn.Write(writeCtx, coderws.MessageText, payload)
  3647. }
  3648. // cyberPolicyRecordedKey guards against double-firing recordCyberPolicyIfMarked
  3649. // within one request (e.g. in a retry/failover loop).
  3650. const cyberPolicyRecordedKey = "ops_cyber_recorded"
  3651. // cyberPolicyOpsErrorMeta carries request-scoped fields captured outside the
  3652. // async goroutine for building the cyber ops_error_logs entry.
  3653. type cyberPolicyOpsErrorMeta struct {
  3654. RequestID string
  3655. ClientRequestID string
  3656. Platform string
  3657. Model string
  3658. RequestPath string
  3659. Stream bool
  3660. InboundEndpoint string
  3661. UserAgent string
  3662. APIKeyPrefix string
  3663. UserID int64
  3664. APIKeyID int64
  3665. AccountID int64
  3666. GroupID *int64
  3667. ClientIP string
  3668. CreatedAt time.Time
  3669. SessionBlockKey string
  3670. }
  3671. // buildCyberPolicyOpsErrorEntry builds the ops_error_logs entry for an upstream
  3672. // cyber_policy hit. StatusCode mirrors what the codex client actually received
  3673. // (400 non-stream / 200 stream), per F6.
  3674. func buildCyberPolicyOpsErrorEntry(meta cyberPolicyOpsErrorMeta, mark *service.CyberPolicyMark) *service.OpsInsertErrorLogInput {
  3675. rt := int16(service.RequestTypeCyberBlocked)
  3676. entry := &service.OpsInsertErrorLogInput{
  3677. RequestID: meta.RequestID,
  3678. ClientRequestID: meta.ClientRequestID,
  3679. Platform: meta.Platform,
  3680. Model: meta.Model,
  3681. RequestPath: meta.RequestPath,
  3682. Stream: meta.Stream,
  3683. InboundEndpoint: meta.InboundEndpoint,
  3684. RequestType: &rt,
  3685. UserAgent: meta.UserAgent,
  3686. APIKeyPrefix: meta.APIKeyPrefix,
  3687. ErrorPhase: "request",
  3688. ErrorType: "cyber_policy",
  3689. Severity: "P3",
  3690. StatusCode: mark.UpstreamStatus,
  3691. IsBusinessLimited: true,
  3692. ErrorMessage: "cyber_policy: " + mark.Message,
  3693. // 原始 body 直接入队;ops service 落库前统一走 sanitizeErrorBodyForStorage 脱敏与截断。
  3694. ErrorBody: mark.Body,
  3695. ErrorSource: "upstream_http",
  3696. ErrorOwner: "provider",
  3697. CreatedAt: meta.CreatedAt,
  3698. }
  3699. if meta.UserID > 0 {
  3700. entry.UserID = &meta.UserID
  3701. }
  3702. if meta.APIKeyID > 0 {
  3703. entry.APIKeyID = &meta.APIKeyID
  3704. }
  3705. if meta.AccountID > 0 {
  3706. entry.AccountID = &meta.AccountID
  3707. }
  3708. entry.GroupID = meta.GroupID
  3709. if meta.ClientIP != "" {
  3710. entry.ClientIP = &meta.ClientIP
  3711. }
  3712. return entry
  3713. }
  3714. // 双语单串:网关客户端面向中英用户,且本错误无 i18n 协商通道。
  3715. const cyberSessionBlockedClientMsg = "该会话已被网络安全策略屏蔽,请开启新会话 / This session is blocked by cyber-security policy, please start a new session"
  3716. // buildCyberSessionBlockedOpsEntry builds the ops_error_logs entry for a request
  3717. // rejected locally by the cyber session block (F5a). Distinct error_type from
  3718. // upstream `cyber_policy`; never feeds moderation logs / violation counting
  3719. // (the request never reached upstream — see spec).
  3720. func buildCyberSessionBlockedOpsEntry(meta cyberPolicyOpsErrorMeta) *service.OpsInsertErrorLogInput {
  3721. rt := int16(service.RequestTypeCyberBlocked)
  3722. entry := &service.OpsInsertErrorLogInput{
  3723. RequestID: meta.RequestID,
  3724. ClientRequestID: meta.ClientRequestID,
  3725. Platform: meta.Platform,
  3726. Model: meta.Model,
  3727. RequestPath: meta.RequestPath,
  3728. Stream: meta.Stream,
  3729. InboundEndpoint: meta.InboundEndpoint,
  3730. RequestType: &rt,
  3731. UserAgent: meta.UserAgent,
  3732. APIKeyPrefix: meta.APIKeyPrefix,
  3733. ErrorPhase: "request",
  3734. ErrorType: "cyber_policy_session_blocked",
  3735. Severity: "P3",
  3736. StatusCode: http.StatusForbidden,
  3737. IsBusinessLimited: true,
  3738. ErrorMessage: "cyber_policy_session_blocked: request rejected locally by session block",
  3739. ErrorSource: "gateway_local",
  3740. ErrorOwner: "platform",
  3741. CreatedAt: meta.CreatedAt,
  3742. // AccountID 有意不设:请求在账号选择前即被拒绝。
  3743. }
  3744. if meta.SessionBlockKey != "" {
  3745. entry.ErrorBody = "session_block_key=" + meta.SessionBlockKey
  3746. }
  3747. if meta.UserID > 0 {
  3748. entry.UserID = &meta.UserID
  3749. }
  3750. if meta.APIKeyID > 0 {
  3751. entry.APIKeyID = &meta.APIKeyID
  3752. }
  3753. entry.GroupID = meta.GroupID
  3754. if meta.ClientIP != "" {
  3755. entry.ClientIP = &meta.ClientIP
  3756. }
  3757. return entry
  3758. }
  3759. // cyberSessionBlockFormat selects the per-endpoint error envelope for a locally
  3760. // blocked session (用户决策:兼容路径各自格式).
  3761. type cyberSessionBlockFormat int
  3762. const (
  3763. cyberBlockFormatResponses cyberSessionBlockFormat = iota
  3764. cyberBlockFormatChat
  3765. cyberBlockFormatAnthropic
  3766. )
  3767. // rejectIfCyberSessionBlocked checks the session-block table BEFORE account
  3768. // selection. Returns true when the request was rejected (response already
  3769. // written + ops entry enqueued). Fail-open: disabled switch / empty key /
  3770. // store error → false.
  3771. func (h *OpenAIGatewayHandler) rejectIfCyberSessionBlocked(c *gin.Context, apiKey *service.APIKey, body []byte, model string, format cyberSessionBlockFormat) bool {
  3772. if h == nil || h.gatewayService == nil || apiKey == nil {
  3773. return false
  3774. }
  3775. // 开关默认关:先走 ~ns 级缓存开关检查,再付出 key 派生(gjson+sha256)成本。
  3776. if enabled, _ := h.gatewayService.CyberSessionBlockRuntime(c.Request.Context()); !enabled {
  3777. return false
  3778. }
  3779. key := findBlockedCyberSessionKey(c.Request.Context(), h.gatewayService, apiKey.ID, c, body)
  3780. if key == "" {
  3781. return false
  3782. }
  3783. // body-signal compact 心跳可能已把响应头提交为 200(cyber 检查在用户槽位
  3784. // 长等待之后执行):以 response.failed 终止事件回传;未提交时停拍后照常
  3785. // 写 JSON(#3887)。
  3786. if service.StopOpenAICompactSSEKeepaliveCommitted(c) {
  3787. service.MarkOpsStreamError(c, "permission_error", cyberSessionBlockedClientMsg, http.StatusForbidden)
  3788. if writeResponsesFailedSSE(c, "permission_error", "", cyberSessionBlockedClientMsg) {
  3789. h.enqueueCyberSessionBlockedOpsEntry(c, apiKey, model, key)
  3790. return true
  3791. }
  3792. }
  3793. switch format {
  3794. case cyberBlockFormatAnthropic:
  3795. c.JSON(http.StatusForbidden, gin.H{"type": "error", "error": gin.H{
  3796. "type": "permission_error",
  3797. "message": cyberSessionBlockedClientMsg,
  3798. }})
  3799. default: // cyberBlockFormatResponses 与 cyberBlockFormatChat:同构的 OpenAI error envelope
  3800. c.JSON(http.StatusForbidden, gin.H{"error": gin.H{
  3801. "type": "permission_error",
  3802. "code": "session_blocked_by_cyber_policy",
  3803. "message": cyberSessionBlockedClientMsg,
  3804. }})
  3805. }
  3806. h.enqueueCyberSessionBlockedOpsEntry(c, apiKey, model, key)
  3807. return true
  3808. }
  3809. type cyberSessionBlockWritePlan struct {
  3810. scopeKey string
  3811. keys []string
  3812. }
  3813. func buildCyberSessionBlockWritePlan(apiKeyID int64, c *gin.Context, body []byte) cyberSessionBlockWritePlan {
  3814. plan := cyberSessionBlockWritePlan{}
  3815. if key := service.CyberSessionExplicitBlockKey(apiKeyID, c, body); key != "" {
  3816. plan.keys = append(plan.keys, key)
  3817. }
  3818. transcriptKeys := service.CyberSessionTranscriptBlockKeys(apiKeyID, body)
  3819. for _, key := range transcriptKeys {
  3820. if len(plan.keys) == 0 || key != plan.keys[0] {
  3821. plan.keys = append(plan.keys, key)
  3822. }
  3823. }
  3824. if len(transcriptKeys) > 0 {
  3825. plan.scopeKey = cyberSessionScopeKey(apiKeyID, c)
  3826. }
  3827. return plan
  3828. }
  3829. func findBlockedCyberSessionKey(ctx context.Context, gatewayService *service.OpenAIGatewayService, apiKeyID int64, c *gin.Context, body []byte) string {
  3830. if gatewayService == nil {
  3831. return ""
  3832. }
  3833. clientIP, userAgent := "", ""
  3834. if c != nil {
  3835. clientIP = strings.TrimSpace(ip.GetClientIP(c))
  3836. userAgent = c.GetHeader("User-Agent")
  3837. }
  3838. return gatewayService.FindCyberSessionBlockedForRequest(ctx, apiKeyID, c, body, clientIP, userAgent)
  3839. }
  3840. func cyberSessionScopeKey(apiKeyID int64, c *gin.Context) string {
  3841. if c == nil {
  3842. return ""
  3843. }
  3844. return service.CyberSessionScopeKey(apiKeyID, strings.TrimSpace(ip.GetClientIP(c)), c.GetHeader("User-Agent"))
  3845. }
  3846. // enqueueCyberSessionBlockedOpsEntry captures request meta and enqueues the
  3847. // ops_error_logs entry for a locally blocked request.
  3848. func (h *OpenAIGatewayHandler) enqueueCyberSessionBlockedOpsEntry(c *gin.Context, apiKey *service.APIKey, model string, sessionBlockKey string) {
  3849. if h.opsService == nil {
  3850. return
  3851. }
  3852. // The dedicated cyber_session_blocked entry owns Ops semantics for this
  3853. // request; suppress the generic middleware record of the same 403 response.
  3854. c.Set(opsDedicatedErrorRecordedKey, true)
  3855. meta := cyberPolicyOpsErrorMeta{Model: model, InboundEndpoint: GetInboundEndpoint(c), CreatedAt: time.Now(), SessionBlockKey: sessionBlockKey}
  3856. meta.RequestID = c.Writer.Header().Get("X-Request-Id")
  3857. if c.Request != nil && c.Request.URL != nil {
  3858. meta.RequestPath = c.Request.URL.Path
  3859. }
  3860. if v, ok := c.Get(opsStreamKey); ok {
  3861. if b, ok := v.(bool); ok {
  3862. meta.Stream = b
  3863. }
  3864. }
  3865. requestCtx := context.Background()
  3866. if c.Request != nil {
  3867. requestCtx = c.Request.Context()
  3868. }
  3869. meta.Platform = resolveOpsPlatform(requestCtx, apiKey, guessPlatformFromPath(meta.RequestPath))
  3870. if c.Request != nil {
  3871. meta.ClientRequestID, _ = c.Request.Context().Value(ctxkey.ClientRequestID).(string)
  3872. meta.UserAgent = c.GetHeader("User-Agent")
  3873. meta.ClientIP = strings.TrimSpace(ip.GetClientIP(c))
  3874. }
  3875. meta.APIKeyID = apiKey.ID
  3876. meta.GroupID = apiKey.GroupID
  3877. meta.APIKeyPrefix = keyPrefix(apiKey.Key, 8)
  3878. if apiKey.User != nil {
  3879. meta.UserID = apiKey.User.ID
  3880. }
  3881. enqueueOpsErrorLog(h.opsService, buildCyberSessionBlockedOpsEntry(meta))
  3882. }
  3883. // recordCyberPolicyIfMarked 在 gateway forward 返回后检查 cyber 标记,异步写风控日志/邮件,
  3884. // 并在 forward 返回错误时写一条 tokens=0 用量行。标记由 gateway 服务层在透传 cyber 后设置;
  3885. // 当前请求已发给用户,本方法只做事后记录,不影响响应。forwardErrored 为 true 时才写用量行,
  3886. // 避免与正常 RecordUsage(forward 成功路径)重复。每请求至多记录一次。
  3887. func (h *OpenAIGatewayHandler) recordCyberPolicyIfMarked(c *gin.Context, apiKey *service.APIKey, account *service.Account, subscription *service.UserSubscription, model string, forwardErrored bool, cyberBlockBody []byte, channelFields service.ChannelUsageFields, requestPayloadHash string) {
  3888. mark := service.GetOpsCyberPolicy(c)
  3889. if mark == nil {
  3890. return
  3891. }
  3892. if c.GetBool(cyberPolicyRecordedKey) {
  3893. return
  3894. }
  3895. c.Set(cyberPolicyRecordedKey, true)
  3896. model = clientRequestedModel(c, model)
  3897. requestID := c.Writer.Header().Get("X-Request-Id")
  3898. var userID, apiKeyID int64
  3899. var userEmail, apiKeyName, groupName string
  3900. var groupID *int64
  3901. if apiKey != nil {
  3902. apiKeyID = apiKey.ID
  3903. apiKeyName = apiKey.Name
  3904. groupID = apiKey.GroupID
  3905. if apiKey.User != nil {
  3906. userID = apiKey.User.ID
  3907. userEmail = apiKey.User.Email
  3908. }
  3909. if apiKey.Group != nil {
  3910. groupName = apiKey.Group.Name
  3911. }
  3912. }
  3913. inboundEndpoint := GetInboundEndpoint(c)
  3914. upstreamEndpoint := ""
  3915. var accountID int64
  3916. if account != nil {
  3917. accountID = account.ID
  3918. upstreamEndpoint = resolveOpenAIUpstreamEndpoint(c, account, nil)
  3919. }
  3920. stream := false
  3921. if v, ok := c.Get(opsStreamKey); ok {
  3922. if b, ok := v.(bool); ok {
  3923. stream = b
  3924. }
  3925. }
  3926. cmSvc := h.contentModerationService
  3927. gwSvc := h.gatewayService
  3928. opsSvc := h.opsService
  3929. apiKeySvc := h.apiKeyService
  3930. requestPath := ""
  3931. if c.Request != nil && c.Request.URL != nil {
  3932. requestPath = c.Request.URL.Path
  3933. }
  3934. requestCtx := context.Background()
  3935. if c.Request != nil {
  3936. requestCtx = c.Request.Context()
  3937. }
  3938. platform := resolveOpsPlatform(requestCtx, apiKey, guessPlatformFromPath(requestPath))
  3939. var clientRequestID, userAgent, clientIPStr string
  3940. if c.Request != nil {
  3941. clientRequestID, _ = c.Request.Context().Value(ctxkey.ClientRequestID).(string)
  3942. userAgent = c.GetHeader("User-Agent")
  3943. clientIPStr = strings.TrimSpace(ip.GetClientIP(c))
  3944. }
  3945. // 提前拍成标量,避免在下方 goroutine 内访问 gin.Context。
  3946. sessionID := service.ExtractClientSessionID(c)
  3947. nativeCompactionV2 := service.IsOpenAINativeCompactionV2(c)
  3948. apiKeyPrefix := ""
  3949. if apiKey != nil {
  3950. apiKeyPrefix = keyPrefix(apiKey.Key, 8)
  3951. }
  3952. opsMeta := cyberPolicyOpsErrorMeta{
  3953. RequestID: requestID,
  3954. ClientRequestID: clientRequestID,
  3955. Platform: platform,
  3956. Model: model,
  3957. RequestPath: requestPath,
  3958. Stream: stream,
  3959. InboundEndpoint: inboundEndpoint,
  3960. UserAgent: userAgent,
  3961. APIKeyPrefix: apiKeyPrefix,
  3962. UserID: userID,
  3963. APIKeyID: apiKeyID,
  3964. AccountID: accountID,
  3965. GroupID: groupID,
  3966. ClientIP: clientIPStr,
  3967. CreatedAt: time.Now(),
  3968. }
  3969. if gwSvc != nil && apiKey != nil {
  3970. plan := buildCyberSessionBlockWritePlan(apiKey.ID, c, cyberBlockBody)
  3971. if len(plan.keys) > 0 {
  3972. blockCtx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
  3973. gwSvc.MarkCyberSessionBlocked(blockCtx, plan.scopeKey, plan.keys)
  3974. cancel()
  3975. }
  3976. }
  3977. go func() {
  3978. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  3979. defer cancel()
  3980. if cmSvc != nil {
  3981. cmSvc.RecordCyberPolicyEvent(ctx, service.CyberPolicyRecordInput{
  3982. RequestID: requestID,
  3983. UserID: userID,
  3984. UserEmail: userEmail,
  3985. APIKeyID: apiKeyID,
  3986. APIKeyName: apiKeyName,
  3987. GroupID: groupID,
  3988. GroupName: groupName,
  3989. Endpoint: inboundEndpoint,
  3990. Model: model,
  3991. UpstreamMessage: mark.Message,
  3992. UpstreamBody: mark.Body,
  3993. UpstreamStatus: mark.UpstreamStatus,
  3994. UpstreamInTok: mark.UpstreamInTok,
  3995. UpstreamOutTok: mark.UpstreamOutTok,
  3996. })
  3997. }
  3998. if forwardErrored && gwSvc != nil {
  3999. gwSvc.RecordCyberPolicyUsageLog(ctx, service.CyberPolicyUsageInput{
  4000. APIKey: apiKey,
  4001. Account: account,
  4002. Subscription: subscription,
  4003. RequestID: requestID,
  4004. Model: model,
  4005. Stream: stream,
  4006. InputTokens: mark.UpstreamInTok,
  4007. OutputTokens: mark.UpstreamOutTok,
  4008. InboundEndpoint: inboundEndpoint,
  4009. UpstreamEndpoint: upstreamEndpoint,
  4010. UserAgent: userAgent,
  4011. IPAddress: clientIPStr,
  4012. SessionID: sessionID,
  4013. RequestPayloadHash: requestPayloadHash,
  4014. APIKeyService: apiKeySvc,
  4015. NativeCompactionV2: nativeCompactionV2,
  4016. ChannelUsageFields: channelFields,
  4017. })
  4018. }
  4019. if opsSvc != nil {
  4020. enqueueOpsErrorLog(opsSvc, buildCyberPolicyOpsErrorEntry(opsMeta, mark))
  4021. }
  4022. }()
  4023. }
  4024. // clearCyberPolicyTurnState resets the cyber mark and recorded guard after a
  4025. // logical WS turn has finished.
  4026. func clearCyberPolicyTurnState(c *gin.Context) {
  4027. clearCyberPolicyAttemptState(c, true)
  4028. }
  4029. func clearCyberPolicyAttemptState(c *gin.Context, resetRecorded bool) {
  4030. if c == nil {
  4031. return
  4032. }
  4033. service.ClearOpsCyberPolicy(c)
  4034. if resetRecorded {
  4035. c.Set(cyberPolicyRecordedKey, false)
  4036. }
  4037. }
  4038. func summarizeWSCloseErrorForLog(err error) (string, string) {
  4039. if err == nil {
  4040. return "-", "-"
  4041. }
  4042. statusCode := coderws.CloseStatus(err)
  4043. if statusCode == -1 {
  4044. return "-", "-"
  4045. }
  4046. closeStatus := fmt.Sprintf("%d(%s)", int(statusCode), statusCode.String())
  4047. closeReason := "-"
  4048. var closeErr coderws.CloseError
  4049. if errors.As(err, &closeErr) {
  4050. reason := strings.TrimSpace(closeErr.Reason)
  4051. if reason != "" {
  4052. closeReason = reason
  4053. }
  4054. }
  4055. return closeStatus, closeReason
  4056. }