openai_ws_forwarder_ingress.go 79 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. "time"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
  13. coderws "github.com/coder/websocket"
  14. "github.com/gin-gonic/gin"
  15. "github.com/tidwall/gjson"
  16. "github.com/tidwall/sjson"
  17. )
  18. type openAIWSRejectedFieldRetryError struct {
  19. body []byte
  20. reason string
  21. }
  22. func (e *openAIWSRejectedFieldRetryError) Error() string {
  23. if e == nil || strings.TrimSpace(e.reason) == "" {
  24. return "retry websocket turn after rejected field normalization"
  25. }
  26. return "retry websocket turn after rejected field normalization: " + e.reason
  27. }
  28. func openAIWSRejectedFieldRetryHTTPStatus(message []byte) int {
  29. for _, value := range gjson.GetManyBytes(message, "status", "status_code", "error.status", "error.status_code") {
  30. status := int(value.Int())
  31. if status >= 100 && status <= 599 {
  32. return status
  33. }
  34. }
  35. return openAIWSErrorHTTPStatus(message)
  36. }
  37. func (s *OpenAIGatewayService) openAIWSIngressInterTurnIdleTimeout() time.Duration {
  38. if s == nil || s.cfg == nil || s.cfg.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds <= 0 {
  39. return 0
  40. }
  41. return time.Duration(s.cfg.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds) * time.Second
  42. }
  43. // newOpenAIWSDownstreamWriteContext binds writes directly to the client
  44. // lifecycle while excluding the separate ingress-lease cancellation signal.
  45. // This lets a lease-loss path finish its current client write before
  46. // ReadOpenAIWSClientMessage sends the retryable close frame.
  47. func newOpenAIWSDownstreamWriteContext(controlCtx context.Context, hooks *OpenAIWSIngressHooks, timeout time.Duration) (context.Context, context.CancelFunc) {
  48. writeParent := controlCtx
  49. if hooks != nil && hooks.ClientLifecycleContext != nil {
  50. writeParent = hooks.ClientLifecycleContext
  51. }
  52. if writeParent == nil {
  53. writeParent = context.Background()
  54. }
  55. return context.WithTimeout(writeParent, timeout)
  56. }
  57. func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
  58. ctx context.Context,
  59. c *gin.Context,
  60. clientConn *coderws.Conn,
  61. account *Account,
  62. token string,
  63. firstClientMessage []byte,
  64. hooks *OpenAIWSIngressHooks,
  65. ) (returnErr error) {
  66. if s == nil {
  67. return errors.New("service is nil")
  68. }
  69. if c == nil {
  70. return errors.New("gin context is nil")
  71. }
  72. if clientConn == nil {
  73. return errors.New("client websocket is nil")
  74. }
  75. if account == nil {
  76. return errors.New("account is nil")
  77. }
  78. // A handler may reuse the same gin context across account failover attempts.
  79. // Never let an OAuth attempt's response aliases leak into the next account.
  80. setCodexToolNameReverse(c, nil)
  81. if _, err := s.prepareCodexAccountIdentitySource(ctx, c, account); err != nil {
  82. return err
  83. }
  84. if err := validateOpenAIWSBearerToken(account, token); err != nil {
  85. return err
  86. }
  87. // 预取一次 OpenAI Fast Policy settings,绑定到 ctx,让该 WS session
  88. // 内所有帧的 evaluateOpenAIFastPolicy 调用复用同一份快照,避免每帧
  89. // 进入 DB / settingRepo。Trade-off 见 withOpenAIFastPolicyContext 注释。
  90. if s.settingService != nil {
  91. if settings, err := s.settingService.GetOpenAIFastPolicySettings(ctx); err == nil && settings != nil {
  92. ctx = withOpenAIFastPolicyContext(ctx, settings)
  93. }
  94. }
  95. // The handler normally owns this registration across retry attempts. Direct
  96. // callers still get the same session-scoped preemption behavior here.
  97. if preemptCtx, cleanupPreempt, armed := s.BeginOpenAIWSIngressSessionPreemption(ctx, c, account, firstClientMessage); armed {
  98. ctx = preemptCtx
  99. defer cleanupPreempt()
  100. defer func() {
  101. if isOpenAIWSSessionPreempted(ctx) {
  102. returnErr = errOpenAIWSSessionPreempted
  103. }
  104. }()
  105. }
  106. wsDecision := s.getOpenAIWSProtocolResolver().Resolve(account)
  107. forceHTTPBridge := account.Platform == PlatformGrok ||
  108. (s.pluginManager != nil && s.pluginManager.ShouldRouteOpenAIOAuth(account))
  109. modeRouterV2Enabled := s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.ModeRouterV2Enabled
  110. ingressMode := OpenAIWSIngressModeCtxPool
  111. if modeRouterV2Enabled && !forceHTTPBridge {
  112. ingressMode = account.ResolveOpenAIResponsesWebSocketV2Mode(s.cfg.Gateway.OpenAIWS.IngressModeDefault)
  113. if ingressMode == OpenAIWSIngressModeOff {
  114. return NewOpenAIWSClientCloseError(
  115. coderws.StatusPolicyViolation,
  116. "websocket mode is disabled for this account",
  117. nil,
  118. )
  119. }
  120. switch ingressMode {
  121. case OpenAIWSIngressModePassthrough:
  122. if wsDecision.Transport != OpenAIUpstreamTransportResponsesWebsocketV2 {
  123. return fmt.Errorf("websocket ingress requires ws_v2 transport, got=%s", wsDecision.Transport)
  124. }
  125. if s.shouldBridgeOpenAIWSPassthroughFirstMessage(account, firstClientMessage) {
  126. forceHTTPBridge = true
  127. break
  128. }
  129. // 首轮准入由握手路径完成;后续 response.create 会在写入上游前
  130. // 依次回调 BeforeRequest 和 BeforeTurn,并在终止或失败时回调
  131. // AfterTurn,从而覆盖 turn 级利润复核、定价冻结和并发槽位释放。
  132. return s.proxyResponsesWebSocketV2Passthrough(
  133. ctx,
  134. c,
  135. clientConn,
  136. account,
  137. token,
  138. firstClientMessage,
  139. hooks,
  140. wsDecision,
  141. )
  142. case OpenAIWSIngressModeHTTPBridge:
  143. forceHTTPBridge = true
  144. case OpenAIWSIngressModeCtxPool, OpenAIWSIngressModeShared, OpenAIWSIngressModeDedicated:
  145. // continue
  146. default:
  147. return NewOpenAIWSClientCloseError(
  148. coderws.StatusPolicyViolation,
  149. "websocket mode only supports ctx_pool/passthrough/http_bridge",
  150. nil,
  151. )
  152. }
  153. }
  154. if !forceHTTPBridge && wsDecision.Transport != OpenAIUpstreamTransportResponsesWebsocketV2 {
  155. return fmt.Errorf("websocket ingress requires ws_v2 transport, got=%s", wsDecision.Transport)
  156. }
  157. dedicatedMode := modeRouterV2Enabled && ingressMode == OpenAIWSIngressModeDedicated
  158. wsURL := ""
  159. wsHost := "-"
  160. wsPath := "-"
  161. if forceHTTPBridge {
  162. wsHost = "xai-http-bridge"
  163. wsPath = "/v1/responses"
  164. } else {
  165. var err error
  166. wsURL, err = s.buildOpenAIResponsesWSURL(account)
  167. if err != nil {
  168. return fmt.Errorf("build ws url: %w", err)
  169. }
  170. if parsedURL, parseErr := url.Parse(wsURL); parseErr == nil && parsedURL != nil {
  171. wsHost = normalizeOpenAIWSLogValue(parsedURL.Host)
  172. wsPath = normalizeOpenAIWSLogValue(parsedURL.Path)
  173. }
  174. }
  175. debugEnabled := isOpenAIWSModeDebugEnabled()
  176. isCodexCLI := openai.IsCodexOfficialClientByHeaders(c.GetHeader("User-Agent"), c.GetHeader("originator")) || (s.cfg != nil && s.cfg.Gateway.ForceCodexCLI)
  177. type openAIWSClientPayload struct {
  178. payloadRaw []byte
  179. accountIdentitySourceRaw []byte
  180. rawForHash []byte
  181. promptCacheKey string
  182. previousResponseID string
  183. originalModel string
  184. imageBillingModel string
  185. imageSizeTier string
  186. imageInputSize string
  187. payloadBytes int
  188. requestedReasoningEffort *string
  189. }
  190. ingressSessionOriginalModel := ""
  191. applyPayloadMutation := func(current []byte, path string, value any) ([]byte, error) {
  192. next, err := sjson.SetBytes(current, path, value)
  193. if err == nil {
  194. return next, nil
  195. }
  196. // 仅在确实需要修改 payload 且 sjson 失败时,退回 map 路径确保兼容性。
  197. payload := make(map[string]any)
  198. if unmarshalErr := json.Unmarshal(current, &payload); unmarshalErr != nil {
  199. return nil, err
  200. }
  201. switch path {
  202. case "type", "model":
  203. payload[path] = value
  204. case "client_metadata." + openAIWSTurnMetadataHeader:
  205. setOpenAIWSTurnMetadata(payload, fmt.Sprintf("%v", value))
  206. default:
  207. return nil, err
  208. }
  209. rebuilt, marshalErr := json.Marshal(payload)
  210. if marshalErr != nil {
  211. return nil, marshalErr
  212. }
  213. return rebuilt, nil
  214. }
  215. parseClientPayload := func(turn int, raw []byte) (openAIWSClientPayload, error) {
  216. trimmed := bytes.TrimSpace(raw)
  217. if len(trimmed) == 0 {
  218. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "empty websocket request payload", nil)
  219. }
  220. if !gjson.ValidBytes(trimmed) {
  221. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", errors.New("invalid json"))
  222. }
  223. values := gjson.GetManyBytes(trimmed, "type", "model", "prompt_cache_key", "previous_response_id")
  224. eventType := strings.TrimSpace(values[0].String())
  225. normalized := trimmed
  226. switch eventType {
  227. case "":
  228. eventType = "response.create"
  229. next, setErr := applyPayloadMutation(normalized, "type", eventType)
  230. if setErr != nil {
  231. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", setErr)
  232. }
  233. normalized = next
  234. case "response.create":
  235. case "response.append":
  236. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
  237. coderws.StatusPolicyViolation,
  238. "response.append is not supported in ws v2; use response.create with previous_response_id",
  239. nil,
  240. )
  241. default:
  242. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
  243. coderws.StatusPolicyViolation,
  244. fmt.Sprintf("unsupported websocket request type: %s", eventType),
  245. nil,
  246. )
  247. }
  248. requestedReasoningEffort := CanonicalRequestedReasoningEffort(normalized, strings.TrimSpace(values[1].String()))
  249. if next, policyErr := applyOpenAIWSReasoningEffortPolicy(normalized, hooks); policyErr != nil {
  250. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, policyErr.Error(), policyErr)
  251. } else {
  252. normalized = next
  253. }
  254. responsesLite := isOpenAIResponsesLiteWebSocketPayload(normalized)
  255. if compatibilityBody, compatibilityChanged, compatibilityErr := normalizeOpenAIResponsesWebSocketCompatibilityBody(normalized, account, responsesLite); compatibilityErr != nil {
  256. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", compatibilityErr)
  257. } else if compatibilityChanged {
  258. normalized = compatibilityBody
  259. }
  260. if account.IsOpenAIOAuthLike() {
  261. aliasedBody, reverse, aliased, aliasErr := aliasOpenAIOAuthReservedToolNamesBody(normalized)
  262. if aliasErr != nil {
  263. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, aliasErr.Error(), aliasErr)
  264. }
  265. updateCodexToolNameReverseForWSFrame(c, normalized, reverse)
  266. if aliased {
  267. normalized = aliasedBody
  268. }
  269. }
  270. originalModel := strings.TrimSpace(values[1].String())
  271. modelMissing := originalModel == ""
  272. if originalModel == "" {
  273. // 入站 WS 长会话里,部分客户端只在第一轮 response.create 上声明
  274. // model,后续 turn 复用同一 session-level model。为避免因省略
  275. // model 直接断开用户连接,这里回落到上一轮已通过校验的客户端模型,
  276. // 并在下方写回上游 payload,保证账号模型映射/fast policy/图片权限
  277. // 仍按同一模型执行。
  278. originalModel = ingressSessionOriginalModel
  279. if originalModel == "" {
  280. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
  281. coderws.StatusPolicyViolation,
  282. "model is required in response.create payload",
  283. nil,
  284. )
  285. }
  286. }
  287. promptCacheKey := strings.TrimSpace(values[2].String())
  288. previousResponseID := strings.TrimSpace(values[3].String())
  289. previousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(previousResponseID)
  290. if previousResponseID != "" && previousResponseIDKind == OpenAIPreviousResponseIDKindMessageID {
  291. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
  292. coderws.StatusPolicyViolation,
  293. "previous_response_id must be a response.id (resp_*), not a message id",
  294. nil,
  295. )
  296. }
  297. if turnMetadata := strings.TrimSpace(c.GetHeader(openAIWSTurnMetadataHeader)); turnMetadata != "" {
  298. next, setErr := applyPayloadMutation(normalized, "client_metadata."+openAIWSTurnMetadataHeader, turnMetadata)
  299. if setErr != nil {
  300. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", setErr)
  301. }
  302. normalized = next
  303. }
  304. accountIdentitySourceRaw := append([]byte(nil), normalized...)
  305. accountScopedPayload, accountScoped, scopeErr := applyCodexAccountAndProtectionIdentityRaw(c, account, normalized)
  306. if scopeErr != nil {
  307. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket identity metadata", scopeErr)
  308. }
  309. if accountScoped {
  310. normalized = accountScopedPayload
  311. }
  312. if responsesLite {
  313. litePayload, _, liteErr := normalizeOpenAIResponsesLitePayloadForAccount(normalized, account)
  314. if liteErr != nil {
  315. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
  316. coderws.StatusPolicyViolation,
  317. liteErr.Error(),
  318. liteErr,
  319. )
  320. }
  321. normalized = litePayload
  322. }
  323. apiKey := getAPIKeyFromContext(c)
  324. imageGenerationAllowed := GroupAllowsImageGeneration(apiKeyGroup(apiKey))
  325. codexImageGenerationExplicitToolPolicy := codexImageGenerationExplicitToolPolicyAllow
  326. if isCodexCLI {
  327. codexImageGenerationExplicitToolPolicy = account.CodexImageGenerationExplicitToolPolicy()
  328. }
  329. codexBridgeEnabled := isCodexCLI &&
  330. !isOpenAIResponsesLiteWebSocketPayload(normalized) &&
  331. imageGenerationAllowed &&
  332. codexImageGenerationExplicitToolPolicy != codexImageGenerationExplicitToolPolicyStrip &&
  333. s.isCodexImageGenerationBridgeEnabled(ctx, account, apiKey)
  334. if codexBridgeEnabled {
  335. payloadMap := make(map[string]any)
  336. if err := decodeOpenAIJSONUseNumber(normalized, &payloadMap); err != nil {
  337. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", err)
  338. }
  339. bridgeModified := false
  340. if ensureOpenAIResponsesImageGenerationTool(payloadMap) {
  341. bridgeModified = true
  342. logOpenAIWSModeInfo("ingress_ws_codex_image_tool_injected account_id=%d", account.ID)
  343. }
  344. if ensureOpenAIResponsesImageGenerationToolChoiceAuto(payloadMap) {
  345. bridgeModified = true
  346. logOpenAIWSModeInfo("ingress_ws_codex_image_tool_choice_auto account_id=%d", account.ID)
  347. }
  348. if normalizeOpenAIResponsesImageGenerationTools(payloadMap) {
  349. bridgeModified = true
  350. }
  351. if applyCodexImageGenerationBridgeInstructions(payloadMap) {
  352. bridgeModified = true
  353. logOpenAIWSModeInfo("ingress_ws_codex_image_bridge_instructions_added account_id=%d", account.ID)
  354. }
  355. if bridgeModified {
  356. rebuilt, marshalErr := json.Marshal(payloadMap)
  357. if marshalErr != nil {
  358. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", marshalErr)
  359. }
  360. normalized = rebuilt
  361. }
  362. }
  363. requestModel := originalModel
  364. if hooks != nil && hooks.MapRequestModel != nil {
  365. mappedModel, mapErr := hooks.MapRequestModel(turn, originalModel)
  366. if mapErr != nil {
  367. return openAIWSClientPayload{}, mapErr
  368. }
  369. if mappedModel = strings.TrimSpace(mappedModel); mappedModel != "" {
  370. requestModel = mappedModel
  371. }
  372. }
  373. upstreamModel := normalizeOpenAIModelForUpstream(account, account.GetMappedModel(requestModel))
  374. if modelMissing || upstreamModel != originalModel {
  375. next, setErr := applyPayloadMutation(normalized, "model", upstreamModel)
  376. if setErr != nil {
  377. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", setErr)
  378. }
  379. normalized = next
  380. }
  381. SetOpsUpstreamModel(c, upstreamModel)
  382. if isCodexCLI && codexImageGenerationExplicitToolPolicy == codexImageGenerationExplicitToolPolicyStrip {
  383. if stripped, changed, stripErr := stripOpenAIImageGenerationToolsFromRawPayload(normalized); stripErr != nil {
  384. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", stripErr)
  385. } else if changed {
  386. normalized = stripped
  387. logOpenAIWSModeInfo("ingress_ws_codex_image_tool_stripped_by_policy account_id=%d", account.ID)
  388. }
  389. }
  390. if stripped, changed, stripErr := stripCodexSparkImageGenerationToolFromRawPayload(normalized, upstreamModel); stripErr != nil {
  391. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", stripErr)
  392. } else if changed {
  393. normalized = stripped
  394. logOpenAIWSModeInfo("ingress_ws_codex_spark_image_tool_stripped account_id=%d", account.ID)
  395. }
  396. imageIntent := IsImageGenerationIntentForPlatform(openAIResponsesEndpoint, originalModel, normalized, account.Platform)
  397. if imageIntent && !imageGenerationAllowed {
  398. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, ImageGenerationPermissionMessage(), nil)
  399. }
  400. imageBillingModel := ""
  401. imageSizeTier := ""
  402. imageInputSize := ""
  403. if imageIntent {
  404. var imageCfgErr error
  405. imageCfg, imageCfgErr := resolveOpenAIResponsesImageBillingConfigDetailedFromBody(normalized, originalModel)
  406. if imageCfgErr != nil {
  407. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, imageCfgErr.Error(), imageCfgErr)
  408. }
  409. imageBillingModel = imageCfg.Model
  410. imageSizeTier = imageCfg.SizeTier
  411. imageInputSize = imageCfg.InputSize
  412. }
  413. // Apply OpenAI Fast Policy on the response.create frame using the same
  414. // evaluator/normalize/scope rules as the HTTP entrypoints. This is the
  415. // single integration point for all WS ingress turns (first + follow-up
  416. // frames flow through here).
  417. //
  418. // Model fallback: first turn still requires model at the handler layer;
  419. // follow-up response.create frames may omit it and then reuse
  420. // ingressSessionOriginalModel. We always write a concrete upstream model
  421. // before evaluating policy, so whitelist / filter behavior remains stable.
  422. policyApplied, blocked, policyErr := s.applyOpenAIFastPolicyToWSResponseCreate(ctx, account, upstreamModel, normalized)
  423. if policyErr != nil {
  424. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", policyErr)
  425. }
  426. if blocked != nil {
  427. MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalPolicyDenied)
  428. // Send a Realtime-style error event to the client first, then
  429. // signal the handler to close the connection with PolicyViolation.
  430. // We intentionally do NOT forward this frame upstream.
  431. //
  432. // coder/websocket@v1.8.14 Conn.Write is synchronous and flushes
  433. // the underlying bufio writer before returning (write.go:42 →
  434. // 307-311), and the subsequent close handshake re-acquires the
  435. // same writeFrameMu, so the error event is guaranteed to reach
  436. // the kernel send buffer before any close frame is queued.
  437. eventBytes := buildOpenAIFastPolicyBlockedWSEvent(blocked)
  438. if eventBytes != nil {
  439. writeCtx, cancel := newOpenAIWSDownstreamWriteContext(ctx, hooks, s.openAIWSWriteTimeout())
  440. _ = clientConn.Write(writeCtx, coderws.MessageText, eventBytes)
  441. cancel()
  442. }
  443. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
  444. coderws.StatusPolicyViolation,
  445. blocked.Message,
  446. blocked,
  447. )
  448. }
  449. normalized = policyApplied
  450. ingressSessionOriginalModel = originalModel
  451. if account.RequestIntegrityMode() != "off" {
  452. if err := checkAccountRequestIntegrity(c, account, trimmed, normalized); err != nil {
  453. return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, err.Error(), err)
  454. }
  455. stageMode1Request(c, account, trimmed)
  456. }
  457. return openAIWSClientPayload{
  458. payloadRaw: normalized,
  459. accountIdentitySourceRaw: accountIdentitySourceRaw,
  460. rawForHash: trimmed,
  461. promptCacheKey: promptCacheKey,
  462. previousResponseID: previousResponseID,
  463. originalModel: originalModel,
  464. imageBillingModel: imageBillingModel,
  465. imageSizeTier: imageSizeTier,
  466. imageInputSize: imageInputSize,
  467. payloadBytes: len(normalized),
  468. requestedReasoningEffort: requestedReasoningEffort,
  469. }, nil
  470. }
  471. writeClientMessage := func(message []byte) error {
  472. writeCtx, cancel := newOpenAIWSDownstreamWriteContext(ctx, hooks, s.openAIWSWriteTimeout())
  473. defer cancel()
  474. message = restoreCodexToolNamesFromContext(c, message)
  475. return clientConn.Write(writeCtx, coderws.MessageText, message)
  476. }
  477. readClientMessage := func() ([]byte, error) {
  478. idleTimeout := s.openAIWSIngressInterTurnIdleTimeout()
  479. msgType, payload, readErr := ReadOpenAIWSClientMessage(
  480. ctx,
  481. clientConn,
  482. idleTimeout,
  483. coderws.StatusNormalClosure,
  484. "websocket idle timeout",
  485. )
  486. if readErr != nil {
  487. var closeErr *OpenAIWSClientCloseError
  488. if errors.As(readErr, &closeErr) && closeErr.StatusCode() == coderws.StatusNormalClosure {
  489. logOpenAIWSModeInfo("ingress_ws_inter_turn_idle_timeout account_id=%d timeout_seconds=%d", account.ID, int(idleTimeout.Seconds()))
  490. }
  491. return nil, readErr
  492. }
  493. if msgType != coderws.MessageText && msgType != coderws.MessageBinary {
  494. return nil, NewOpenAIWSClientCloseError(
  495. coderws.StatusPolicyViolation,
  496. fmt.Sprintf("unsupported websocket client message type: %s", msgType.String()),
  497. nil,
  498. )
  499. }
  500. return payload, nil
  501. }
  502. firstPayload, err := parseClientPayload(1, firstClientMessage)
  503. if err != nil {
  504. return err
  505. }
  506. useHTTPBridge := forceHTTPBridge || s.shouldBridgeOpenAIWSHTTP(account, firstPayload.payloadBytes, firstPayload.previousResponseID)
  507. turnState := strings.TrimSpace(c.GetHeader(openAIWSTurnStateHeader))
  508. stateStore := s.getOpenAIWSStateStore()
  509. groupID := getOpenAIGroupIDFromContext(c)
  510. storeDisabledConnMode := s.openAIWSStoreDisabledConnMode()
  511. sessionHash := ""
  512. preferredConnID := ""
  513. storeDisabled := false
  514. refreshIngressRouteState := func(payload openAIWSClientPayload) {
  515. sessionHash = s.GenerateSessionHash(c, payload.rawForHash)
  516. preferredConnID = ""
  517. storeDisabled = s.isOpenAIWSStoreDisabledInRequestRaw(payload.payloadRaw, account)
  518. if useHTTPBridge {
  519. // Sticky account affinity may be shared, but an HTTP bridge must not
  520. // inherit another connection's native WS turn state or socket binding.
  521. return
  522. }
  523. if turnState == "" && stateStore != nil && sessionHash != "" {
  524. if savedTurnState, ok := stateStore.GetSessionTurnState(groupID, sessionHash); ok {
  525. turnState = savedTurnState
  526. }
  527. }
  528. if stateStore != nil && payload.previousResponseID != "" {
  529. if connID, ok := stateStore.GetResponseConn(payload.previousResponseID); ok {
  530. preferredConnID = connID
  531. }
  532. }
  533. if stateStore != nil && storeDisabled && payload.previousResponseID == "" && sessionHash != "" {
  534. if connID, ok := stateStore.GetSessionConn(groupID, sessionHash); ok {
  535. preferredConnID = connID
  536. }
  537. }
  538. }
  539. refreshIngressRouteState(firstPayload)
  540. if useHTTPBridge {
  541. logOpenAIWSModeInfo(
  542. "ingress_ws_http_bridge_start account_id=%d account_type=%s payload_bytes=%d threshold_bytes=%d has_session_hash=%v store_disabled=%v",
  543. account.ID,
  544. account.Type,
  545. firstPayload.payloadBytes,
  546. s.openAIWSHTTPBridgeThresholdBytes(),
  547. sessionHash != "",
  548. storeDisabled,
  549. )
  550. currentBridgePayload := firstPayload
  551. // Keep the first turn as the stable conversation seed. The mapped model
  552. // is resolved again for each turn below so an in-connection model switch
  553. // cannot reuse another model's upstream cache identity.
  554. grokCacheSeedPayload := firstPayload.payloadRaw
  555. var bridgeReplayInput []json.RawMessage
  556. bridgeReplayInputExists := false
  557. var bridgeAccountFailoverInput []json.RawMessage
  558. bridgeAccountFailoverInputExists := false
  559. for turn := 1; ; turn++ {
  560. if turn > 1 && hooks != nil && hooks.BeforeRequest != nil {
  561. if err := hooks.BeforeRequest(turn, currentBridgePayload.payloadRaw, currentBridgePayload.originalModel); err != nil {
  562. return err
  563. }
  564. }
  565. if hooks != nil && hooks.BeforeTurn != nil {
  566. if err := hooks.BeforeTurn(turn); err != nil {
  567. return err
  568. }
  569. }
  570. if turnState != "" && c != nil && c.Request != nil {
  571. c.Request.Header.Set(openAIWSTurnStateHeader, turnState)
  572. }
  573. if c != nil && sessionHash != "" {
  574. c.Set(openAIWSIngressSessionHashContextKey, sessionHash)
  575. }
  576. // 剥离本会话已知失效的加密项,阻断同一失效密文随历史反复触发上游拒绝。
  577. // 历史序列须同步剥离,否则与已剥离的当前 input 项错位,prefix 复用失配。
  578. if invalidDigests := s.sessionInvalidEncryptedContentDigests(groupID, sessionHash); len(invalidDigests) > 0 {
  579. strippedPayload, strippedCount := s.stripSessionInvalidEncryptedContentLogged(
  580. currentBridgePayload.payloadRaw, invalidDigests, "ingress_ws_http_bridge_invalid_encrypted_lineage_strip", account.ID, turn,
  581. )
  582. if strippedCount > 0 {
  583. currentBridgePayload.payloadRaw = strippedPayload
  584. currentBridgePayload.payloadBytes = len(strippedPayload)
  585. }
  586. if bridgeReplayInputExists {
  587. bridgeReplayInput, _ = stripOpenAIInvalidEncryptedContentFromReplayItems(bridgeReplayInput, invalidDigests)
  588. }
  589. if bridgeAccountFailoverInputExists {
  590. bridgeAccountFailoverInput, _ = stripOpenAIInvalidEncryptedContentFromReplayItems(bridgeAccountFailoverInput, invalidDigests)
  591. }
  592. }
  593. bridgePayloadRaw := currentBridgePayload.payloadRaw
  594. bridgePayloadBytes := currentBridgePayload.payloadBytes
  595. toolOutputCoverage := AnalyzeToolCallOutputContextCoverageBytes(currentBridgePayload.payloadRaw)
  596. needsBridgeReplay := currentBridgePayload.previousResponseID != "" ||
  597. (toolOutputCoverage.HasFunctionCallOutput && !toolOutputCoverage.ContextCoversAllCallIDs)
  598. // 一次解析当前 input,正常 replay 与 account-failover 两份序列共享同一批正文。
  599. bridgeCurrentItems, bridgeCurrentItemsExist, extractErr := openAIWSExtractNormalizedInputSequence(
  600. currentBridgePayload.payloadRaw,
  601. )
  602. if extractErr != nil {
  603. return fmt.Errorf("build websocket http bridge replay input: %w", extractErr)
  604. }
  605. turnReplayInput, turnReplayInputExists := buildOpenAIWSReplayInputSequenceFromItems(
  606. bridgeReplayInput,
  607. bridgeReplayInputExists,
  608. bridgeCurrentItems,
  609. bridgeCurrentItemsExist,
  610. needsBridgeReplay,
  611. )
  612. turnAccountFailoverInput, turnAccountFailoverInputExists := buildOpenAIWSReplayInputSequenceFromItems(
  613. bridgeAccountFailoverInput,
  614. bridgeAccountFailoverInputExists,
  615. bridgeCurrentItems,
  616. bridgeCurrentItemsExist,
  617. needsBridgeReplay,
  618. )
  619. if needsBridgeReplay && turnReplayInputExists {
  620. updatedPayload, setInputErr := setOpenAIWSPayloadInputSequence(
  621. currentBridgePayload.payloadRaw,
  622. turnReplayInput,
  623. true,
  624. )
  625. if setInputErr != nil {
  626. return fmt.Errorf("set websocket http bridge replay input: %w", setInputErr)
  627. }
  628. bridgePayloadRaw = updatedPayload
  629. bridgePayloadBytes = len(updatedPayload)
  630. logOpenAIWSModeInfo(
  631. "ingress_ws_http_bridge_replay_input account_id=%d turn=%d input_items=%d previous_response_id_present=%v has_tool_output=%v",
  632. account.ID,
  633. turn,
  634. len(turnReplayInput),
  635. currentBridgePayload.previousResponseID != "",
  636. openAIWSRawPayloadHasToolCallOutput(currentBridgePayload.payloadRaw),
  637. )
  638. }
  639. grokCacheIdentity := ""
  640. if account.Platform == PlatformGrok {
  641. grokCacheIdentity, err = resolveGrokWSCacheIdentity(
  642. c,
  643. account,
  644. grokCacheSeedPayload,
  645. currentBridgePayload.payloadRaw,
  646. currentBridgePayload.originalModel,
  647. )
  648. if err != nil {
  649. return fmt.Errorf("resolve Grok websocket cache identity: %w", err)
  650. }
  651. }
  652. if err := validateMode1StagedRequest(c, account, bridgePayloadRaw); err != nil {
  653. return err
  654. }
  655. result, bridgeErr := s.proxyOpenAIWSHTTPBridgeTurn(
  656. ctx,
  657. c,
  658. account,
  659. token,
  660. bridgePayloadRaw,
  661. bridgePayloadBytes,
  662. currentBridgePayload.originalModel,
  663. currentBridgePayload.imageBillingModel,
  664. currentBridgePayload.imageSizeTier,
  665. currentBridgePayload.imageInputSize,
  666. grokCacheIdentity,
  667. turn,
  668. writeClientMessage,
  669. )
  670. if bridgeErr != nil && isOpenAIWSSessionPreempted(ctx) {
  671. return errOpenAIWSSessionPreempted
  672. }
  673. if hooks != nil && hooks.AfterTurn != nil {
  674. hooks.AfterTurn(turn, result, bridgeErr)
  675. }
  676. if bridgeErr != nil {
  677. var failoverErr *UpstreamFailoverError
  678. if turn > 1 && errors.As(bridgeErr, &failoverErr) && failoverErr != nil {
  679. retryPayload, retrySafe, retryPayloadErr := buildOpenAIWSCurrentTurnRetryPayload(
  680. currentBridgePayload.accountIdentitySourceRaw,
  681. turnAccountFailoverInput,
  682. turnAccountFailoverInputExists,
  683. currentBridgePayload.originalModel,
  684. )
  685. if retryPayloadErr != nil {
  686. return fmt.Errorf("build websocket current-turn failover payload: %w", retryPayloadErr)
  687. }
  688. if !retrySafe {
  689. retryPayload = nil
  690. }
  691. return newOpenAIWSCurrentTurnFailoverError(bridgeErr, retryPayload)
  692. }
  693. return bridgeErr
  694. }
  695. if result == nil {
  696. return errors.New("websocket http bridge turn result is nil")
  697. }
  698. // turnReplayInput/turnAccountFailoverInput 可能共享同一头数组(转移自
  699. // bridgeCurrentItems),保存历史必须经 combine 新建头,禁止就地 append。
  700. bridgeReplayInput = turnReplayInput
  701. bridgeReplayInputExists = turnReplayInputExists
  702. if result.wsReplayInputExists {
  703. bridgeReplayInput = combineOpenAIWSReplayItems(bridgeReplayInput, result.wsReplayInput)
  704. bridgeReplayInputExists = true
  705. }
  706. bridgeAccountFailoverInput = turnAccountFailoverInput
  707. bridgeAccountFailoverInputExists = turnAccountFailoverInputExists
  708. if len(result.wsAccountFailoverReplayInput) > 0 {
  709. bridgeAccountFailoverInput = combineOpenAIWSReplayItems(
  710. bridgeAccountFailoverInput,
  711. result.wsAccountFailoverReplayInput,
  712. )
  713. bridgeAccountFailoverInputExists = true
  714. }
  715. if bridgeTurnState := strings.TrimSpace(result.ResponseHeaders.Get(openAIWSTurnStateHeader)); bridgeTurnState != "" {
  716. // Follow-up turns on this bridge retain their own upstream state;
  717. // publishing it by session hash would leak it to independent bridges.
  718. turnState = bridgeTurnState
  719. }
  720. responseID := strings.TrimSpace(result.RequestID)
  721. if responseID != "" && stateStore != nil {
  722. ttl := s.openAIWSResponseStickyTTL()
  723. logOpenAIWSBindResponseAccountWarn(groupID, account.ID, responseID, stateStore.BindResponseAccount(ctx, groupID, responseID, account.ID, ttl))
  724. }
  725. nextClientMessage, readErr := readClientMessage()
  726. if readErr != nil {
  727. if isOpenAIWSSessionPreempted(ctx) {
  728. return errOpenAIWSSessionPreempted
  729. }
  730. if isOpenAIWSClientDisconnectError(readErr) {
  731. closeStatus, closeReason := summarizeOpenAIWSReadCloseError(readErr)
  732. logOpenAIWSModeInfo(
  733. "ingress_ws_http_bridge_client_closed account_id=%d close_status=%s close_reason=%s",
  734. account.ID,
  735. closeStatus,
  736. truncateOpenAIWSLogValue(closeReason, openAIWSHeaderValueMaxLen),
  737. )
  738. return nil
  739. }
  740. return fmt.Errorf("read client websocket request: %w", readErr)
  741. }
  742. nextPayload, parseErr := parseClientPayload(turn+1, nextClientMessage)
  743. if parseErr != nil {
  744. return parseErr
  745. }
  746. currentBridgePayload = nextPayload
  747. }
  748. }
  749. firstRoutingFields := gjson.GetManyBytes(firstPayload.payloadRaw, "model", "service_tier")
  750. wsHeaders, _, buildHdrErr := s.buildOpenAIWSHeaders(
  751. ctx,
  752. c,
  753. account,
  754. token,
  755. wsDecision,
  756. isCodexCLI,
  757. turnState,
  758. strings.TrimSpace(c.GetHeader(openAIWSTurnMetadataHeader)),
  759. firstPayload.promptCacheKey,
  760. firstRoutingFields[0].String(),
  761. firstRoutingFields[1].String(),
  762. )
  763. if buildHdrErr != nil {
  764. return fmt.Errorf("build ws headers: %w", buildHdrErr)
  765. }
  766. baseAcquireReq := openAIWSAcquireRequest{
  767. Account: account,
  768. WSURL: wsURL,
  769. Headers: wsHeaders,
  770. HeadersFactory: func(factoryCtx context.Context, headers http.Header) (http.Header, error) {
  771. return s.refreshOpenAIAgentIdentityHeaders(factoryCtx, account, headers)
  772. },
  773. ProxyURL: func() string {
  774. if account.ProxyID != nil && account.Proxy != nil {
  775. return account.Proxy.URL()
  776. }
  777. return ""
  778. }(),
  779. ForceNewConn: false,
  780. }
  781. pool := s.getOpenAIWSConnPool()
  782. if pool == nil {
  783. return errors.New("openai ws conn pool is nil")
  784. }
  785. logOpenAIWSModeInfo(
  786. "ingress_ws_protocol_confirm account_id=%d account_type=%s transport=%s ws_host=%s ws_path=%s ws_mode=%s store_disabled=%v has_session_hash=%v has_previous_response_id=%v",
  787. account.ID,
  788. account.Type,
  789. normalizeOpenAIWSLogValue(string(wsDecision.Transport)),
  790. wsHost,
  791. wsPath,
  792. normalizeOpenAIWSLogValue(ingressMode),
  793. storeDisabled,
  794. sessionHash != "",
  795. firstPayload.previousResponseID != "",
  796. )
  797. if debugEnabled {
  798. logOpenAIWSModeDebug(
  799. "ingress_ws_start account_id=%d account_type=%s transport=%s ws_host=%s preferred_conn_id=%s has_session_hash=%v has_previous_response_id=%v store_disabled=%v",
  800. account.ID,
  801. account.Type,
  802. normalizeOpenAIWSLogValue(string(wsDecision.Transport)),
  803. wsHost,
  804. truncateOpenAIWSLogValue(preferredConnID, openAIWSIDValueMaxLen),
  805. sessionHash != "",
  806. firstPayload.previousResponseID != "",
  807. storeDisabled,
  808. )
  809. }
  810. if firstPayload.previousResponseID != "" {
  811. firstPreviousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(firstPayload.previousResponseID)
  812. logOpenAIWSModeInfo(
  813. "ingress_ws_continuation_probe account_id=%d turn=%d previous_response_id=%s previous_response_id_kind=%s preferred_conn_id=%s session_hash=%s header_session_id=%s header_conversation_id=%s has_turn_state=%v turn_state_len=%d has_prompt_cache_key=%v store_disabled=%v",
  814. account.ID,
  815. 1,
  816. truncateOpenAIWSLogValue(firstPayload.previousResponseID, openAIWSIDValueMaxLen),
  817. normalizeOpenAIWSLogValue(firstPreviousResponseIDKind),
  818. truncateOpenAIWSLogValue(preferredConnID, openAIWSIDValueMaxLen),
  819. truncateOpenAIWSLogValue(sessionHash, 12),
  820. openAIWSHeaderValueForLog(baseAcquireReq.Headers, "session_id"),
  821. openAIWSHeaderValueForLog(baseAcquireReq.Headers, "conversation_id"),
  822. turnState != "",
  823. len(turnState),
  824. firstPayload.promptCacheKey != "",
  825. storeDisabled,
  826. )
  827. }
  828. acquireTimeout := s.openAIWSAcquireTimeout()
  829. if acquireTimeout <= 0 {
  830. acquireTimeout = 30 * time.Second
  831. }
  832. agentTaskRecoveryTried := false
  833. var acquireTurnLease func(int, string, bool) (*openAIWSConnLease, error)
  834. acquireTurnLease = func(turn int, preferred string, forcePreferredConn bool) (*openAIWSConnLease, error) {
  835. req := cloneOpenAIWSAcquireRequest(baseAcquireReq)
  836. req.PreferredConnID = strings.TrimSpace(preferred)
  837. req.ForcePreferredConn = forcePreferredConn
  838. // dedicated 模式下每次获取均新建连接,避免跨会话复用残留上下文。
  839. req.ForceNewConn = dedicatedMode
  840. acquireCtx, acquireCancel := context.WithTimeout(ctx, acquireTimeout)
  841. lease, acquireErr := pool.Acquire(acquireCtx, req)
  842. acquireCancel()
  843. var dialErr *openAIWSDialError
  844. if acquireErr != nil && s.isAgentIdentityAccount(ctx, account) && errors.As(acquireErr, &dialErr) && isAgentIdentityTaskInvalidWSDialError(dialErr) && !agentTaskRecoveryTried {
  845. agentTaskRecoveryTried = true
  846. if recoveryErr := s.recoverAgentIdentityTask(ctx, account, account.GetCredential("task_id")); recoveryErr != nil {
  847. return nil, fmt.Errorf("agent identity task recovery failed: %w", recoveryErr)
  848. }
  849. return acquireTurnLease(turn, preferred, forcePreferredConn)
  850. }
  851. if acquireErr != nil {
  852. if isOpenAIWSSessionPreempted(ctx) {
  853. return nil, errOpenAIWSSessionPreempted
  854. }
  855. canonicalModel := canonicalOpenAIAccountSchedulingModel(account, ingressSessionOriginalModel)
  856. s.handleOpenAIWSDialTransientFailure(ctx, account, canonicalModel, acquireErr)
  857. dialStatus, dialClass, dialCloseStatus, dialCloseReason, dialRespServer, dialRespVia, dialRespCFRay, dialRespReqID := summarizeOpenAIWSDialError(acquireErr)
  858. logOpenAIWSModeInfo(
  859. "ingress_ws_upstream_acquire_fail account_id=%d turn=%d reason=%s dial_status=%d dial_class=%s dial_close_status=%s dial_close_reason=%s dial_resp_server=%s dial_resp_via=%s dial_resp_cf_ray=%s dial_resp_x_request_id=%s cause=%s preferred_conn_id=%s force_preferred_conn=%v ws_host=%s ws_path=%s proxy_enabled=%v",
  860. account.ID,
  861. turn,
  862. normalizeOpenAIWSLogValue(classifyOpenAIWSAcquireError(acquireErr)),
  863. dialStatus,
  864. dialClass,
  865. dialCloseStatus,
  866. truncateOpenAIWSLogValue(dialCloseReason, openAIWSHeaderValueMaxLen),
  867. dialRespServer,
  868. dialRespVia,
  869. dialRespCFRay,
  870. dialRespReqID,
  871. truncateOpenAIWSLogValue(acquireErr.Error(), openAIWSLogValueMaxLen),
  872. truncateOpenAIWSLogValue(preferred, openAIWSIDValueMaxLen),
  873. forcePreferredConn,
  874. wsHost,
  875. wsPath,
  876. account.ProxyID != nil && account.Proxy != nil,
  877. )
  878. var dialErr *openAIWSDialError
  879. if errors.As(acquireErr, &dialErr) && dialErr != nil && dialErr.StatusCode == http.StatusTooManyRequests {
  880. s.persistOpenAIWSRateLimitSignal(ctx, account, dialErr.ResponseHeaders, nil, "rate_limit_exceeded", "rate_limit_error", strings.TrimSpace(acquireErr.Error()), canonicalModel)
  881. return nil, s.newOpenAIWSRateLimitFailoverError(account, dialErr.ResponseHeaders, nil, acquireErr.Error())
  882. }
  883. if errors.Is(acquireErr, errOpenAIWSPreferredConnUnavailable) {
  884. return nil, NewOpenAIWSClientCloseError(
  885. coderws.StatusPolicyViolation,
  886. "upstream continuation connection is unavailable; please restart the conversation",
  887. acquireErr,
  888. )
  889. }
  890. if errors.Is(acquireErr, context.DeadlineExceeded) || errors.Is(acquireErr, errOpenAIWSConnQueueFull) {
  891. return nil, NewOpenAIWSClientCloseError(
  892. coderws.StatusTryAgainLater,
  893. "upstream websocket is busy, please retry later",
  894. acquireErr,
  895. )
  896. }
  897. return nil, acquireErr
  898. }
  899. connID := strings.TrimSpace(lease.ConnID())
  900. if handshakeTurnState := strings.TrimSpace(lease.HandshakeHeader(openAIWSTurnStateHeader)); handshakeTurnState != "" {
  901. turnState = handshakeTurnState
  902. if stateStore != nil && sessionHash != "" {
  903. stateStore.BindSessionTurnState(groupID, sessionHash, handshakeTurnState, s.openAIWSSessionStickyTTL())
  904. }
  905. updatedHeaders := cloneHeader(baseAcquireReq.Headers)
  906. if updatedHeaders == nil {
  907. updatedHeaders = make(http.Header)
  908. }
  909. updatedHeaders.Set(openAIWSTurnStateHeader, handshakeTurnState)
  910. baseAcquireReq.Headers = updatedHeaders
  911. }
  912. logOpenAIWSModeInfo(
  913. "ingress_ws_upstream_connected account_id=%d turn=%d conn_id=%s conn_reused=%v conn_pick_ms=%d queue_wait_ms=%d preferred_conn_id=%s",
  914. account.ID,
  915. turn,
  916. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  917. lease.Reused(),
  918. lease.ConnPickDuration().Milliseconds(),
  919. lease.QueueWaitDuration().Milliseconds(),
  920. truncateOpenAIWSLogValue(preferred, openAIWSIDValueMaxLen),
  921. )
  922. return lease, nil
  923. }
  924. var rejectedFieldRetryState *openAIResponsesRejectedFieldRetryState
  925. sendAndRelay := func(turn int, lease *openAIWSConnLease, payload []byte, payloadBytes int, originalModel string, imageBillingModel string, imageSizeTier string, imageInputSize string, requestedReasoningEffort *string) (trafficResult *OpenAIForwardResult, trafficErr error) {
  926. ctx, permit, admissionErr := beginAccountTrafficTurn(ctx, s.httpUpstream, account)
  927. if admissionErr != nil {
  928. return nil, admissionErr
  929. }
  930. defer func() { finishAccountTrafficTurn(permit, trafficErr) }()
  931. responseModelObserver := &upstreamResponseModelObserver{}
  932. if lease == nil {
  933. return nil, errors.New("upstream websocket lease is nil")
  934. }
  935. turnStart := time.Now()
  936. wroteDownstream := false
  937. if err := validateMode1StagedRequest(c, account, payload); err != nil {
  938. return nil, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, err.Error(), err)
  939. }
  940. if err := lease.WriteJSONWithContextTimeout(ctx, json.RawMessage(payload), s.openAIWSWriteTimeout()); err != nil {
  941. return nil, wrapOpenAIWSIngressTurnError(
  942. "write_upstream",
  943. fmt.Errorf("write upstream websocket request: %w", err),
  944. false,
  945. )
  946. }
  947. if debugEnabled {
  948. logOpenAIWSModeDebug(
  949. "ingress_ws_turn_request_sent account_id=%d turn=%d conn_id=%s payload_bytes=%d",
  950. account.ID,
  951. turn,
  952. truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
  953. payloadBytes,
  954. )
  955. }
  956. responseID := ""
  957. usage := OpenAIUsage{}
  958. imageCounter := newOpenAIImageOutputCounter()
  959. var firstTokenMs *int
  960. reqStream := openAIWSPayloadBoolFromRaw(payload, "stream", true)
  961. turnPreviousResponseID := openAIWSPayloadStringFromRaw(payload, "previous_response_id")
  962. turnPreviousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(turnPreviousResponseID)
  963. turnPromptCacheKey := openAIWSPayloadStringFromRaw(payload, "prompt_cache_key")
  964. turnStoreDisabled := s.isOpenAIWSStoreDisabledInRequestRaw(payload, account)
  965. turnHasFunctionCallOutput := openAIWSRawPayloadHasToolCallOutput(payload)
  966. eventCount := 0
  967. tokenEventCount := 0
  968. terminalEventCount := 0
  969. replayCollector := &openAIWSToolCallReplayCollector{}
  970. firstEventType := ""
  971. lastEventType := ""
  972. needModelReplace := false
  973. clientDisconnected := false
  974. mappedModel := ""
  975. var mappedModelBytes []byte
  976. if originalModel != "" {
  977. mappedModel = strings.TrimSpace(gjson.GetBytes(payload, "model").String())
  978. if mappedModel == "" {
  979. mappedModel = normalizeOpenAIModelForUpstream(account, account.GetMappedModel(originalModel))
  980. }
  981. needModelReplace = mappedModel != "" && mappedModel != originalModel
  982. if needModelReplace {
  983. mappedModelBytes = []byte(mappedModel)
  984. }
  985. }
  986. for {
  987. upstreamMessage, readErr := lease.ReadMessageWithContextTimeout(ctx, s.openAIWSReadTimeout())
  988. if readErr != nil {
  989. lease.MarkBroken()
  990. return nil, wrapOpenAIWSIngressTurnError(
  991. "read_upstream",
  992. fmt.Errorf("read upstream websocket event: %w", readErr),
  993. wroteDownstream,
  994. )
  995. }
  996. if normalized, changed := normalizeCompletedImageGenerationStatus(upstreamMessage); changed {
  997. upstreamMessage = normalized
  998. }
  999. eventType, eventResponseID, _ := parseOpenAIWSEventEnvelope(upstreamMessage)
  1000. responseModelObserver.ObserveOpenAI(upstreamMessage, eventType)
  1001. if responseID == "" && eventResponseID != "" {
  1002. responseID = eventResponseID
  1003. }
  1004. if eventType != "" {
  1005. eventCount++
  1006. if firstEventType == "" {
  1007. firstEventType = eventType
  1008. }
  1009. lastEventType = eventType
  1010. }
  1011. if openAIWSMessageShouldParseUsage(eventType, upstreamMessage) {
  1012. parseOpenAIWSResponseUsageFromCompletedEvent(upstreamMessage, &usage)
  1013. }
  1014. if eventType == "error" || eventType == "response.failed" {
  1015. markOpenAICyberPolicyEvent(c, upstreamMessage, http.StatusOK, &usage)
  1016. }
  1017. if eventType == "error" {
  1018. s.handleOpenAIWSErrorEventTransientFailure(ctx, account, mappedModel, lease.HandshakeHeaders(), upstreamMessage)
  1019. errCodeRaw, errTypeRaw, errMsgRaw := parseOpenAIWSErrorEventFields(upstreamMessage)
  1020. statusCode := openAIWSRejectedFieldRetryHTTPStatus(upstreamMessage)
  1021. if !wroteDownstream && statusCode == http.StatusBadRequest && rejectedFieldRetryState != nil {
  1022. retryBody, retryReason, changed, retryErr := normalizeOpenAIResponsesRejectedFieldRetryBody(
  1023. statusCode,
  1024. payload,
  1025. upstreamMessage,
  1026. )
  1027. if retryErr != nil {
  1028. return nil, fmt.Errorf("normalize websocket rejected field retry: %w", retryErr)
  1029. }
  1030. if changed && rejectedFieldRetryState.Allow(retryBody) {
  1031. logOpenAIWSModeInfo(
  1032. "ingress_ws_rejected_field_retry account_id=%d turn=%d conn_id=%s reason=%s",
  1033. account.ID,
  1034. turn,
  1035. truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
  1036. truncateOpenAIWSLogValue(retryReason, openAIWSLogValueMaxLen),
  1037. )
  1038. return nil, &openAIWSRejectedFieldRetryError{
  1039. body: append([]byte(nil), retryBody...),
  1040. reason: retryReason,
  1041. }
  1042. }
  1043. }
  1044. s.persistOpenAIWSRateLimitSignal(ctx, account, lease.HandshakeHeaders(), upstreamMessage, errCodeRaw, errTypeRaw, errMsgRaw, mappedModel)
  1045. fallbackReason, _ := classifyOpenAIWSErrorEventFromRaw(errCodeRaw, errTypeRaw, errMsgRaw)
  1046. if fallbackReason == openAIWSFallbackReasonInvalidEncryptedContent {
  1047. // 记录被上游拒绝的密文摘要;错误照旧透传,下一轮进场时按摘要预剥离。
  1048. if digests := collectOpenAIEncryptedContentDigestsRaw(payload); len(digests) > 0 {
  1049. s.markOpenAIWSInvalidEncryptedContentLineage(groupID, sessionHash, digests)
  1050. logOpenAIWSModeInfo(
  1051. "ingress_ws_invalid_encrypted_lineage_mark account_id=%d turn=%d digests=%d",
  1052. account.ID,
  1053. turn,
  1054. len(digests),
  1055. )
  1056. }
  1057. }
  1058. errCode, errType, errMessage := summarizeOpenAIWSErrorEventFieldsFromRaw(errCodeRaw, errTypeRaw, errMsgRaw)
  1059. recoverablePrevNotFound := fallbackReason == openAIWSIngressStagePreviousResponseNotFound &&
  1060. turnPreviousResponseID != "" &&
  1061. !turnHasFunctionCallOutput &&
  1062. s.openAIWSIngressPreviousResponseRecoveryEnabled() &&
  1063. !wroteDownstream
  1064. if recoverablePrevNotFound {
  1065. // 可恢复场景使用非 error 关键字日志,避免被 LegacyPrintf 误判为 ERROR 级别。
  1066. logOpenAIWSModeInfo(
  1067. "ingress_ws_prev_response_recoverable account_id=%d turn=%d conn_id=%s idx=%d reason=%s code=%s type=%s message=%s previous_response_id=%s previous_response_id_kind=%s response_id=%s store_disabled=%v has_prompt_cache_key=%v",
  1068. account.ID,
  1069. turn,
  1070. truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
  1071. eventCount,
  1072. truncateOpenAIWSLogValue(fallbackReason, openAIWSLogValueMaxLen),
  1073. errCode,
  1074. errType,
  1075. errMessage,
  1076. truncateOpenAIWSLogValue(turnPreviousResponseID, openAIWSIDValueMaxLen),
  1077. normalizeOpenAIWSLogValue(turnPreviousResponseIDKind),
  1078. truncateOpenAIWSLogValue(responseID, openAIWSIDValueMaxLen),
  1079. turnStoreDisabled,
  1080. turnPromptCacheKey != "",
  1081. )
  1082. } else {
  1083. logOpenAIWSModeInfo(
  1084. "ingress_ws_error_event account_id=%d turn=%d conn_id=%s idx=%d fallback_reason=%s err_code=%s err_type=%s err_message=%s previous_response_id=%s previous_response_id_kind=%s response_id=%s store_disabled=%v has_prompt_cache_key=%v",
  1085. account.ID,
  1086. turn,
  1087. truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
  1088. eventCount,
  1089. truncateOpenAIWSLogValue(fallbackReason, openAIWSLogValueMaxLen),
  1090. errCode,
  1091. errType,
  1092. errMessage,
  1093. truncateOpenAIWSLogValue(turnPreviousResponseID, openAIWSIDValueMaxLen),
  1094. normalizeOpenAIWSLogValue(turnPreviousResponseIDKind),
  1095. truncateOpenAIWSLogValue(responseID, openAIWSIDValueMaxLen),
  1096. turnStoreDisabled,
  1097. turnPromptCacheKey != "",
  1098. )
  1099. }
  1100. // previous_response_not_found 在 ingress 模式支持单次恢复重试:
  1101. // 不把该 error 直接下发客户端,而是由上层去掉 previous_response_id 后重放当前 turn。
  1102. if recoverablePrevNotFound {
  1103. lease.MarkBroken()
  1104. errMsg := strings.TrimSpace(errMsgRaw)
  1105. if errMsg == "" {
  1106. errMsg = "previous response not found"
  1107. }
  1108. return nil, wrapOpenAIWSIngressTurnError(
  1109. openAIWSIngressStagePreviousResponseNotFound,
  1110. errors.New(errMsg),
  1111. false,
  1112. )
  1113. }
  1114. if !wroteDownstream && isOpenAIWSRateLimitError(errCodeRaw, errTypeRaw, errMsgRaw) {
  1115. lease.MarkBroken()
  1116. return nil, s.newOpenAIWSRateLimitFailoverError(account, lease.HandshakeHeaders(), upstreamMessage, errMsgRaw)
  1117. }
  1118. }
  1119. isTokenEvent := isOpenAIWSTokenEvent(eventType)
  1120. if isTokenEvent {
  1121. tokenEventCount++
  1122. }
  1123. isTerminalEvent := isOpenAIWSTerminalEvent(eventType)
  1124. if isTerminalEvent {
  1125. terminalEventCount++
  1126. }
  1127. if firstTokenMs == nil && isTokenEvent {
  1128. ms := int(time.Since(turnStart).Milliseconds())
  1129. firstTokenMs = &ms
  1130. }
  1131. imageCounter.AddSSEData(upstreamMessage)
  1132. if !clientDisconnected {
  1133. if needModelReplace && len(mappedModelBytes) > 0 && openAIWSEventMayContainModel(eventType) && bytes.Contains(upstreamMessage, mappedModelBytes) {
  1134. upstreamMessage = replaceOpenAIWSMessageModel(upstreamMessage, mappedModel, originalModel)
  1135. }
  1136. if openAIWSEventMayContainToolCalls(eventType) && openAIWSMessageLikelyContainsToolCalls(upstreamMessage) {
  1137. if corrected, changed := s.toolCorrector.CorrectToolCallsInSSEBytes(upstreamMessage); changed {
  1138. upstreamMessage = corrected
  1139. }
  1140. }
  1141. replayCollector.AddEvent(eventType, upstreamMessage)
  1142. // 客户端写出副本改写容量降载码:Codex 对 error/response.failed 中的
  1143. // server_is_overloaded / slow_down 判致命并终止会话,改写后走客户端
  1144. // 内置退避重试。HTTP/SSE(openai_gateway_response_handling.go)与
  1145. // http_bridge(openai_ws_http_bridge.go)两条路径早已这么做,
  1146. // ctx_pool 的 ingress 直写路径是唯一漏掉的一条 —— 同一个上游降载
  1147. // 事件在这里会让会话就地终止,切到 http_bridge 却能正常退避重试。
  1148. //
  1149. // 必须写进独立变量而不是原地改 upstreamMessage:下面的
  1150. // markOpenAIWSClientVisibleFailure 与 handleOpenAIWSTerminalTransientFailure
  1151. // 仍要按未改写的原始 payload 判定账号状态,这正是
  1152. // sanitizeOpenAICapacityShedErrorCodeForClient 注释里写明的前提。
  1153. clientMessage := upstreamMessage
  1154. if eventType == "error" || eventType == "response.failed" {
  1155. if rewritten, changed := sanitizeOpenAICapacityShedErrorCodeForClient(clientMessage); changed {
  1156. clientMessage = rewritten
  1157. }
  1158. }
  1159. if err := writeClientMessage(clientMessage); err != nil {
  1160. if isOpenAIWSClientDisconnectError(err) {
  1161. clientDisconnected = true
  1162. closeStatus, closeReason := summarizeOpenAIWSReadCloseError(err)
  1163. logOpenAIWSModeInfo(
  1164. "ingress_ws_client_disconnected_drain account_id=%d turn=%d conn_id=%s close_status=%s close_reason=%s",
  1165. account.ID,
  1166. turn,
  1167. truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
  1168. closeStatus,
  1169. truncateOpenAIWSLogValue(closeReason, openAIWSHeaderValueMaxLen),
  1170. )
  1171. } else {
  1172. return nil, wrapOpenAIWSIngressTurnError(
  1173. "write_client",
  1174. fmt.Errorf("write client websocket event: %w", err),
  1175. wroteDownstream,
  1176. )
  1177. }
  1178. } else {
  1179. wroteDownstream = true
  1180. markOpenAIWSClientVisibleFailure(c, eventType, upstreamMessage)
  1181. }
  1182. }
  1183. if isTerminalEvent {
  1184. terminalEvent := s.handleOpenAIWSTerminalTransientFailure(ctx, account, mappedModel, lease.HandshakeHeaders(), upstreamMessage)
  1185. // 客户端已断连时,上游连接的 session 状态不可信,标记 broken 避免回池复用。
  1186. if clientDisconnected {
  1187. lease.MarkBroken()
  1188. }
  1189. firstTokenMsValue := -1
  1190. if firstTokenMs != nil {
  1191. firstTokenMsValue = *firstTokenMs
  1192. }
  1193. if debugEnabled {
  1194. logOpenAIWSModeDebug(
  1195. "ingress_ws_turn_completed account_id=%d turn=%d conn_id=%s response_id=%s duration_ms=%d events=%d token_events=%d terminal_events=%d first_event=%s last_event=%s first_token_ms=%d client_disconnected=%v",
  1196. account.ID,
  1197. turn,
  1198. truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
  1199. truncateOpenAIWSLogValue(responseID, openAIWSIDValueMaxLen),
  1200. time.Since(turnStart).Milliseconds(),
  1201. eventCount,
  1202. tokenEventCount,
  1203. terminalEventCount,
  1204. truncateOpenAIWSLogValue(firstEventType, openAIWSLogValueMaxLen),
  1205. truncateOpenAIWSLogValue(lastEventType, openAIWSLogValueMaxLen),
  1206. firstTokenMsValue,
  1207. clientDisconnected,
  1208. )
  1209. }
  1210. imageCount := imageCounter.Count()
  1211. result := &OpenAIForwardResult{
  1212. RequestID: responseID,
  1213. Usage: usage,
  1214. Model: originalModel,
  1215. UpstreamModel: mappedModel,
  1216. UpstreamResponseModel: responseModelObserver.Model(),
  1217. UpstreamResponseModelConflict: responseModelObserver.Conflict(),
  1218. UpstreamResponseServiceTier: responseModelObserver.ServiceTier(),
  1219. ServiceTier: resolvedOpenAIUpstreamServiceTierFromObserver(responseModelObserver, extractOpenAIServiceTierFromBody(payload)),
  1220. ReasoningEffort: ApplyThinkingEnabledFallback(extractOpenAIReasoningEffortFromBody(payload, mappedModel, originalModel), payload, mappedModel),
  1221. RequestedReasoningEffort: requestedReasoningEffort,
  1222. Stream: reqStream,
  1223. OpenAIWSMode: true,
  1224. UpstreamTerminalEvent: terminalEvent,
  1225. ResponseHeaders: lease.HandshakeHeaders(),
  1226. Duration: time.Since(turnStart),
  1227. FirstTokenMs: firstTokenMs,
  1228. }
  1229. if replayInput := replayCollector.Items(); len(replayInput) > 0 {
  1230. result.wsReplayInput = replayInput
  1231. result.wsReplayInputExists = true
  1232. }
  1233. if imageCount > 0 {
  1234. result.ImageCount = imageCount
  1235. result.ImageSize = imageSizeTier
  1236. result.ImageInputSize = imageInputSize
  1237. result.ImageOutputSizes = imageCounter.Sizes()
  1238. result.BillingModel = imageBillingModel
  1239. }
  1240. return result, nil
  1241. }
  1242. }
  1243. }
  1244. currentPayload := firstPayload.payloadRaw
  1245. currentOriginalModel := firstPayload.originalModel
  1246. currentImageBillingModel := firstPayload.imageBillingModel
  1247. currentImageSizeTier := firstPayload.imageSizeTier
  1248. currentImageInputSize := firstPayload.imageInputSize
  1249. currentPayloadBytes := firstPayload.payloadBytes
  1250. currentRequestedReasoningEffort := firstPayload.requestedReasoningEffort
  1251. isStrictAffinityTurn := func(payload []byte) bool {
  1252. if !storeDisabled {
  1253. return false
  1254. }
  1255. return strings.TrimSpace(openAIWSPayloadStringFromRaw(payload, "previous_response_id")) != ""
  1256. }
  1257. var sessionLease *openAIWSConnLease
  1258. sessionConnID := ""
  1259. pinnedSessionConnID := ""
  1260. unpinSessionConn := func(connID string) {
  1261. connID = strings.TrimSpace(connID)
  1262. if connID == "" || pinnedSessionConnID != connID {
  1263. return
  1264. }
  1265. pool.UnpinConn(account.ID, connID)
  1266. pinnedSessionConnID = ""
  1267. }
  1268. pinSessionConn := func(connID string) {
  1269. if !storeDisabled {
  1270. return
  1271. }
  1272. connID = strings.TrimSpace(connID)
  1273. if connID == "" || pinnedSessionConnID == connID {
  1274. return
  1275. }
  1276. if pinnedSessionConnID != "" {
  1277. pool.UnpinConn(account.ID, pinnedSessionConnID)
  1278. pinnedSessionConnID = ""
  1279. }
  1280. if pool.PinConn(account.ID, connID) {
  1281. pinnedSessionConnID = connID
  1282. }
  1283. }
  1284. // lastTurnClean 标记最后一轮 sendAndRelay 是否正常完成(收到终端事件且客户端未断连)。
  1285. // 所有异常路径(读写错误、error 事件、客户端断连)已在各自分支或上层(L3403)中 MarkBroken,
  1286. // 因此 releaseSessionLease 中只需在非正常结束时 MarkBroken。
  1287. lastTurnClean := false
  1288. releaseSessionLease := func() {
  1289. if sessionLease == nil {
  1290. return
  1291. }
  1292. if !lastTurnClean {
  1293. sessionLease.MarkBroken()
  1294. }
  1295. unpinSessionConn(sessionConnID)
  1296. sessionLease.Release()
  1297. if debugEnabled {
  1298. logOpenAIWSModeDebug(
  1299. "ingress_ws_upstream_released account_id=%d conn_id=%s",
  1300. account.ID,
  1301. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1302. )
  1303. }
  1304. }
  1305. defer releaseSessionLease()
  1306. turn := 1
  1307. rejectedFieldRetryState = newOpenAIResponsesRejectedFieldRetryState(currentPayload)
  1308. turnRetry := 0
  1309. turnPrevRecoveryTried := false
  1310. lastTurnFinishedAt := time.Time{}
  1311. lastTurnResponseID := ""
  1312. lastTurnPayload := []byte(nil)
  1313. var lastTurnStrictState *openAIWSIngressPreviousTurnStrictState
  1314. lastTurnReplayInput := []json.RawMessage(nil)
  1315. lastTurnReplayInputExists := false
  1316. currentTurnReplayInput := []json.RawMessage(nil)
  1317. currentTurnReplayInputExists := false
  1318. skipBeforeTurn := false
  1319. hasCurrentOrReplayFunctionCallOutput := func(payload []byte) bool {
  1320. if openAIWSRawPayloadHasToolCallOutput(payload) {
  1321. return true
  1322. }
  1323. return currentTurnReplayInputExists && openAIWSRawItemsHasFunctionCallOutput(currentTurnReplayInput)
  1324. }
  1325. resetSessionLease := func(markBroken bool) {
  1326. if sessionLease == nil {
  1327. return
  1328. }
  1329. if markBroken {
  1330. sessionLease.MarkBroken()
  1331. }
  1332. releaseSessionLease()
  1333. sessionLease = nil
  1334. sessionConnID = ""
  1335. preferredConnID = ""
  1336. }
  1337. recoverIngressPrevResponseNotFound := func(relayErr error, turn int, connID string) bool {
  1338. if !isOpenAIWSIngressPreviousResponseNotFound(relayErr) {
  1339. return false
  1340. }
  1341. if turnPrevRecoveryTried || !s.openAIWSIngressPreviousResponseRecoveryEnabled() {
  1342. return false
  1343. }
  1344. // 携带 function_call_output 的请求不能丢弃 previous_response_id:
  1345. // 上游 API 需要 response chain 来匹配 tool_result 与之前的 tool_use,
  1346. // 丢弃后会导致 "No tool call found for function call output" 400 错误。
  1347. if hasCurrentOrReplayFunctionCallOutput(currentPayload) {
  1348. return false
  1349. }
  1350. if isStrictAffinityTurn(currentPayload) {
  1351. // Layer 2:严格亲和链路命中 previous_response_not_found 时,降级为“去掉 previous_response_id 后重放一次”。
  1352. // 该错误说明续链锚点已失效,继续 strict fail-close 只会直接中断本轮请求。
  1353. logOpenAIWSModeInfo(
  1354. "ingress_ws_prev_response_recovery_layer2 account_id=%d turn=%d conn_id=%s store_disabled_conn_mode=%s action=drop_previous_response_id_retry",
  1355. account.ID,
  1356. turn,
  1357. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  1358. normalizeOpenAIWSLogValue(storeDisabledConnMode),
  1359. )
  1360. }
  1361. turnPrevRecoveryTried = true
  1362. updatedPayload, removed, dropErr := dropPreviousResponseIDFromRawPayload(currentPayload)
  1363. if dropErr != nil || !removed {
  1364. reason := "not_removed"
  1365. if dropErr != nil {
  1366. reason = "drop_error"
  1367. }
  1368. logOpenAIWSModeInfo(
  1369. "ingress_ws_prev_response_recovery_skip account_id=%d turn=%d conn_id=%s reason=%s",
  1370. account.ID,
  1371. turn,
  1372. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  1373. normalizeOpenAIWSLogValue(reason),
  1374. )
  1375. return false
  1376. }
  1377. updatedWithInput, setInputErr := setOpenAIWSPayloadInputSequence(
  1378. updatedPayload,
  1379. currentTurnReplayInput,
  1380. currentTurnReplayInputExists,
  1381. )
  1382. if setInputErr != nil {
  1383. logOpenAIWSModeInfo(
  1384. "ingress_ws_prev_response_recovery_skip account_id=%d turn=%d conn_id=%s reason=set_full_input_error cause=%s",
  1385. account.ID,
  1386. turn,
  1387. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  1388. truncateOpenAIWSLogValue(setInputErr.Error(), openAIWSLogValueMaxLen),
  1389. )
  1390. return false
  1391. }
  1392. logOpenAIWSModeInfo(
  1393. "ingress_ws_prev_response_recovery account_id=%d turn=%d conn_id=%s action=drop_previous_response_id retry=1",
  1394. account.ID,
  1395. turn,
  1396. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  1397. )
  1398. currentPayload = updatedWithInput
  1399. currentPayloadBytes = len(updatedWithInput)
  1400. resetSessionLease(true)
  1401. skipBeforeTurn = true
  1402. return true
  1403. }
  1404. retryIngressTurn := func(relayErr error, turn int, connID string) bool {
  1405. if !isOpenAIWSIngressTurnRetryable(relayErr) || turnRetry >= 1 {
  1406. return false
  1407. }
  1408. if isStrictAffinityTurn(currentPayload) {
  1409. logOpenAIWSModeInfo(
  1410. "ingress_ws_turn_retry_skip account_id=%d turn=%d conn_id=%s reason=strict_affinity",
  1411. account.ID,
  1412. turn,
  1413. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  1414. )
  1415. return false
  1416. }
  1417. turnRetry++
  1418. logOpenAIWSModeInfo(
  1419. "ingress_ws_turn_retry account_id=%d turn=%d retry=%d reason=%s conn_id=%s",
  1420. account.ID,
  1421. turn,
  1422. turnRetry,
  1423. truncateOpenAIWSLogValue(openAIWSIngressTurnRetryReason(relayErr), openAIWSLogValueMaxLen),
  1424. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  1425. )
  1426. resetSessionLease(true)
  1427. skipBeforeTurn = true
  1428. return true
  1429. }
  1430. for {
  1431. if turn > 1 && !skipBeforeTurn && hooks != nil && hooks.BeforeRequest != nil {
  1432. if err := hooks.BeforeRequest(turn, currentPayload, currentOriginalModel); err != nil {
  1433. return err
  1434. }
  1435. }
  1436. if !skipBeforeTurn && hooks != nil && hooks.BeforeTurn != nil {
  1437. if err := hooks.BeforeTurn(turn); err != nil {
  1438. return err
  1439. }
  1440. }
  1441. skipBeforeTurn = false
  1442. // 剥离本会话已知失效的加密项,阻断同一失效密文随历史反复触发上游拒绝。
  1443. // 历史序列须同步剥离,否则与已剥离的当前 input 项错位,prefix 复用失配。
  1444. if invalidDigests := s.sessionInvalidEncryptedContentDigests(groupID, sessionHash); len(invalidDigests) > 0 {
  1445. strippedPayload, strippedCount := s.stripSessionInvalidEncryptedContentLogged(
  1446. currentPayload, invalidDigests, "ingress_ws_invalid_encrypted_lineage_strip", account.ID, turn,
  1447. )
  1448. if strippedCount > 0 {
  1449. currentPayload = strippedPayload
  1450. currentPayloadBytes = len(strippedPayload)
  1451. }
  1452. if lastTurnReplayInputExists {
  1453. lastTurnReplayInput, _ = stripOpenAIInvalidEncryptedContentFromReplayItems(lastTurnReplayInput, invalidDigests)
  1454. }
  1455. }
  1456. currentPreviousResponseID := openAIWSPayloadStringFromRaw(currentPayload, "previous_response_id")
  1457. expectedPrev := strings.TrimSpace(lastTurnResponseID)
  1458. toolSignals := ToolContinuationSignals{
  1459. HasFunctionCallOutput: openAIWSRawPayloadHasToolCallOutput(currentPayload),
  1460. }
  1461. if toolSignals.HasFunctionCallOutput {
  1462. var currentReqBody map[string]any
  1463. if err := json.Unmarshal(currentPayload, &currentReqBody); err == nil {
  1464. toolSignals = AnalyzeToolContinuationSignals(currentReqBody)
  1465. }
  1466. }
  1467. hasFunctionCallOutput := toolSignals.HasFunctionCallOutput
  1468. // store=false + function_call_output 场景必须有续链锚点。
  1469. // 若客户端未传 previous_response_id,优先回填上一轮响应 ID,避免上游报 call_id 无法关联。
  1470. if shouldInferIngressFunctionCallOutputPreviousResponseID(
  1471. storeDisabled,
  1472. turn,
  1473. toolSignals,
  1474. currentPreviousResponseID,
  1475. expectedPrev,
  1476. ) {
  1477. updatedPayload, setPrevErr := setPreviousResponseIDToRawPayload(currentPayload, expectedPrev)
  1478. if setPrevErr != nil {
  1479. logOpenAIWSModeInfo(
  1480. "ingress_ws_function_call_output_prev_infer_skip account_id=%d turn=%d conn_id=%s reason=set_previous_response_id_error cause=%s expected_previous_response_id=%s",
  1481. account.ID,
  1482. turn,
  1483. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1484. truncateOpenAIWSLogValue(setPrevErr.Error(), openAIWSLogValueMaxLen),
  1485. truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
  1486. )
  1487. } else {
  1488. currentPayload = updatedPayload
  1489. currentPayloadBytes = len(updatedPayload)
  1490. currentPreviousResponseID = expectedPrev
  1491. logOpenAIWSModeInfo(
  1492. "ingress_ws_function_call_output_prev_infer account_id=%d turn=%d conn_id=%s action=set_previous_response_id previous_response_id=%s",
  1493. account.ID,
  1494. turn,
  1495. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1496. truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
  1497. )
  1498. }
  1499. }
  1500. nextReplayInput, nextReplayInputExists, replayInputErr := buildOpenAIWSReplayInputSequence(
  1501. lastTurnReplayInput,
  1502. lastTurnReplayInputExists,
  1503. currentPayload,
  1504. currentPreviousResponseID != "",
  1505. )
  1506. if replayInputErr != nil {
  1507. logOpenAIWSModeInfo(
  1508. "ingress_ws_replay_input_skip account_id=%d turn=%d conn_id=%s reason=build_error cause=%s",
  1509. account.ID,
  1510. turn,
  1511. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1512. truncateOpenAIWSLogValue(replayInputErr.Error(), openAIWSLogValueMaxLen),
  1513. )
  1514. currentTurnReplayInput = nil
  1515. currentTurnReplayInputExists = false
  1516. } else {
  1517. currentTurnReplayInput = nextReplayInput
  1518. currentTurnReplayInputExists = nextReplayInputExists
  1519. }
  1520. replayHasFunctionCallOutput := currentTurnReplayInputExists &&
  1521. openAIWSRawItemsHasFunctionCallOutput(currentTurnReplayInput)
  1522. hasFunctionCallOutput = hasFunctionCallOutput || replayHasFunctionCallOutput
  1523. if storeDisabled && turn > 1 && currentPreviousResponseID != "" {
  1524. shouldKeepPreviousResponseID := false
  1525. strictReason := ""
  1526. var strictErr error
  1527. if lastTurnStrictState != nil {
  1528. shouldKeepPreviousResponseID, strictReason, strictErr = shouldKeepIngressPreviousResponseIDWithStrictState(
  1529. lastTurnStrictState,
  1530. currentPayload,
  1531. lastTurnResponseID,
  1532. hasFunctionCallOutput,
  1533. )
  1534. } else {
  1535. shouldKeepPreviousResponseID, strictReason, strictErr = shouldKeepIngressPreviousResponseID(
  1536. lastTurnPayload,
  1537. currentPayload,
  1538. lastTurnResponseID,
  1539. hasFunctionCallOutput,
  1540. )
  1541. }
  1542. if strictErr != nil {
  1543. logOpenAIWSModeInfo(
  1544. "ingress_ws_prev_response_strict_eval account_id=%d turn=%d conn_id=%s action=keep_previous_response_id reason=%s cause=%s previous_response_id=%s expected_previous_response_id=%s has_function_call_output=%v",
  1545. account.ID,
  1546. turn,
  1547. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1548. normalizeOpenAIWSLogValue(strictReason),
  1549. truncateOpenAIWSLogValue(strictErr.Error(), openAIWSLogValueMaxLen),
  1550. truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
  1551. truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
  1552. hasFunctionCallOutput,
  1553. )
  1554. } else if !shouldKeepPreviousResponseID {
  1555. updatedPayload, removed, dropErr := dropPreviousResponseIDFromRawPayload(currentPayload)
  1556. if dropErr != nil || !removed {
  1557. dropReason := "not_removed"
  1558. if dropErr != nil {
  1559. dropReason = "drop_error"
  1560. }
  1561. logOpenAIWSModeInfo(
  1562. "ingress_ws_prev_response_strict_eval account_id=%d turn=%d conn_id=%s action=keep_previous_response_id reason=%s drop_reason=%s previous_response_id=%s expected_previous_response_id=%s has_function_call_output=%v",
  1563. account.ID,
  1564. turn,
  1565. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1566. normalizeOpenAIWSLogValue(strictReason),
  1567. normalizeOpenAIWSLogValue(dropReason),
  1568. truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
  1569. truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
  1570. hasFunctionCallOutput,
  1571. )
  1572. } else {
  1573. updatedWithInput, setInputErr := setOpenAIWSPayloadInputSequence(
  1574. updatedPayload,
  1575. currentTurnReplayInput,
  1576. currentTurnReplayInputExists,
  1577. )
  1578. if setInputErr != nil {
  1579. logOpenAIWSModeInfo(
  1580. "ingress_ws_prev_response_strict_eval account_id=%d turn=%d conn_id=%s action=keep_previous_response_id reason=%s drop_reason=set_full_input_error previous_response_id=%s expected_previous_response_id=%s cause=%s has_function_call_output=%v",
  1581. account.ID,
  1582. turn,
  1583. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1584. normalizeOpenAIWSLogValue(strictReason),
  1585. truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
  1586. truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
  1587. truncateOpenAIWSLogValue(setInputErr.Error(), openAIWSLogValueMaxLen),
  1588. hasFunctionCallOutput,
  1589. )
  1590. } else {
  1591. currentPayload = updatedWithInput
  1592. currentPayloadBytes = len(updatedWithInput)
  1593. logOpenAIWSModeInfo(
  1594. "ingress_ws_prev_response_strict_eval account_id=%d turn=%d conn_id=%s action=drop_previous_response_id_full_create reason=%s previous_response_id=%s expected_previous_response_id=%s has_function_call_output=%v",
  1595. account.ID,
  1596. turn,
  1597. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1598. normalizeOpenAIWSLogValue(strictReason),
  1599. truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
  1600. truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
  1601. hasFunctionCallOutput,
  1602. )
  1603. currentPreviousResponseID = ""
  1604. }
  1605. }
  1606. }
  1607. }
  1608. forcePreferredConn := isStrictAffinityTurn(currentPayload)
  1609. if sessionLease == nil {
  1610. acquiredLease, acquireErr := acquireTurnLease(turn, preferredConnID, forcePreferredConn)
  1611. if acquireErr != nil {
  1612. return fmt.Errorf("acquire upstream websocket: %w", acquireErr)
  1613. }
  1614. sessionLease = acquiredLease
  1615. sessionConnID = strings.TrimSpace(sessionLease.ConnID())
  1616. if storeDisabled {
  1617. pinSessionConn(sessionConnID)
  1618. } else {
  1619. unpinSessionConn(sessionConnID)
  1620. }
  1621. }
  1622. shouldPreflightPing := turn > 1 && sessionLease != nil && sessionLease.SupportsIdlePingWithoutReader() && turnRetry == 0
  1623. if shouldPreflightPing && openAIWSIngressPreflightPingIdle > 0 && !lastTurnFinishedAt.IsZero() {
  1624. if time.Since(lastTurnFinishedAt) < openAIWSIngressPreflightPingIdle {
  1625. shouldPreflightPing = false
  1626. }
  1627. }
  1628. if shouldPreflightPing {
  1629. if pingErr := sessionLease.PingWithTimeout(openAIWSConnHealthCheckTO); pingErr != nil {
  1630. logOpenAIWSModeInfo(
  1631. "ingress_ws_upstream_preflight_ping_fail account_id=%d turn=%d conn_id=%s cause=%s",
  1632. account.ID,
  1633. turn,
  1634. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1635. truncateOpenAIWSLogValue(pingErr.Error(), openAIWSLogValueMaxLen),
  1636. )
  1637. if forcePreferredConn {
  1638. // 携带 function_call_output 的请求不能丢弃 previous_response_id:
  1639. // 上游 API 需要 response chain 来匹配 tool_result 与之前的 tool_use,
  1640. // 除非 replay input 已经包含与每个 tool_result 匹配的 tool_use 上下文。
  1641. hasFCOutput := hasFunctionCallOutput
  1642. hasReplayToolContext := hasFCOutput &&
  1643. currentTurnReplayInputExists &&
  1644. openAIWSRawItemsHaveToolCallContextForOutputs(currentTurnReplayInput)
  1645. if !turnPrevRecoveryTried && currentPreviousResponseID != "" && (!hasFCOutput || hasReplayToolContext) {
  1646. updatedPayload, removed, dropErr := dropPreviousResponseIDFromRawPayload(currentPayload)
  1647. if dropErr != nil || !removed {
  1648. reason := "not_removed"
  1649. if dropErr != nil {
  1650. reason = "drop_error"
  1651. }
  1652. logOpenAIWSModeInfo(
  1653. "ingress_ws_preflight_ping_recovery_skip account_id=%d turn=%d conn_id=%s reason=%s previous_response_id=%s",
  1654. account.ID,
  1655. turn,
  1656. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1657. normalizeOpenAIWSLogValue(reason),
  1658. truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
  1659. )
  1660. } else {
  1661. updatedWithInput, setInputErr := setOpenAIWSPayloadInputSequence(
  1662. updatedPayload,
  1663. currentTurnReplayInput,
  1664. currentTurnReplayInputExists,
  1665. )
  1666. if setInputErr != nil {
  1667. logOpenAIWSModeInfo(
  1668. "ingress_ws_preflight_ping_recovery_skip account_id=%d turn=%d conn_id=%s reason=set_full_input_error previous_response_id=%s cause=%s",
  1669. account.ID,
  1670. turn,
  1671. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1672. truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
  1673. truncateOpenAIWSLogValue(setInputErr.Error(), openAIWSLogValueMaxLen),
  1674. )
  1675. } else {
  1676. logOpenAIWSModeInfo(
  1677. "ingress_ws_preflight_ping_recovery account_id=%d turn=%d conn_id=%s action=drop_previous_response_id_retry previous_response_id=%s has_function_call_output=%v has_replay_tool_context=%v",
  1678. account.ID,
  1679. turn,
  1680. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1681. truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
  1682. hasFCOutput,
  1683. hasReplayToolContext,
  1684. )
  1685. turnPrevRecoveryTried = true
  1686. currentPayload = updatedWithInput
  1687. currentPayloadBytes = len(updatedWithInput)
  1688. resetSessionLease(true)
  1689. skipBeforeTurn = true
  1690. continue
  1691. }
  1692. }
  1693. }
  1694. if hasFCOutput && currentPreviousResponseID != "" {
  1695. reason := "function_call_output_missing_replay_context"
  1696. if hasReplayToolContext {
  1697. reason = "function_call_output_replay_not_applied"
  1698. }
  1699. logOpenAIWSModeInfo(
  1700. "ingress_ws_preflight_ping_recovery_skip account_id=%d turn=%d conn_id=%s reason=%s action=fail_close previous_response_id=%s has_replay_tool_context=%v",
  1701. account.ID,
  1702. turn,
  1703. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1704. reason,
  1705. truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
  1706. hasReplayToolContext,
  1707. )
  1708. }
  1709. resetSessionLease(true)
  1710. return NewOpenAIWSClientCloseError(
  1711. coderws.StatusPolicyViolation,
  1712. "upstream continuation connection is unavailable; please restart the conversation",
  1713. pingErr,
  1714. )
  1715. }
  1716. resetSessionLease(true)
  1717. acquiredLease, acquireErr := acquireTurnLease(turn, preferredConnID, forcePreferredConn)
  1718. if acquireErr != nil {
  1719. return fmt.Errorf("acquire upstream websocket after preflight ping fail: %w", acquireErr)
  1720. }
  1721. sessionLease = acquiredLease
  1722. sessionConnID = strings.TrimSpace(sessionLease.ConnID())
  1723. if storeDisabled {
  1724. pinSessionConn(sessionConnID)
  1725. }
  1726. }
  1727. }
  1728. connID := sessionConnID
  1729. if currentPreviousResponseID != "" {
  1730. chainedFromLast := expectedPrev != "" && currentPreviousResponseID == expectedPrev
  1731. currentPreviousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(currentPreviousResponseID)
  1732. logOpenAIWSModeInfo(
  1733. "ingress_ws_turn_chain account_id=%d turn=%d conn_id=%s previous_response_id=%s previous_response_id_kind=%s last_turn_response_id=%s chained_from_last=%v preferred_conn_id=%s header_session_id=%s header_conversation_id=%s has_turn_state=%v turn_state_len=%d has_prompt_cache_key=%v store_disabled=%v",
  1734. account.ID,
  1735. turn,
  1736. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  1737. truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
  1738. normalizeOpenAIWSLogValue(currentPreviousResponseIDKind),
  1739. truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
  1740. chainedFromLast,
  1741. truncateOpenAIWSLogValue(preferredConnID, openAIWSIDValueMaxLen),
  1742. openAIWSHeaderValueForLog(baseAcquireReq.Headers, "session_id"),
  1743. openAIWSHeaderValueForLog(baseAcquireReq.Headers, "conversation_id"),
  1744. turnState != "",
  1745. len(turnState),
  1746. openAIWSPayloadStringFromRaw(currentPayload, "prompt_cache_key") != "",
  1747. storeDisabled,
  1748. )
  1749. }
  1750. result, relayErr := sendAndRelay(turn, sessionLease, currentPayload, currentPayloadBytes, currentOriginalModel, currentImageBillingModel, currentImageSizeTier, currentImageInputSize, currentRequestedReasoningEffort)
  1751. if relayErr != nil {
  1752. lastTurnClean = false
  1753. if isOpenAIWSSessionPreempted(ctx) {
  1754. sessionLease.MarkBroken()
  1755. return errOpenAIWSSessionPreempted
  1756. }
  1757. var rejectedFieldErr *openAIWSRejectedFieldRetryError
  1758. if errors.As(relayErr, &rejectedFieldErr) && rejectedFieldErr != nil && len(rejectedFieldErr.body) > 0 {
  1759. currentPayload = append([]byte(nil), rejectedFieldErr.body...)
  1760. currentPayloadBytes = len(currentPayload)
  1761. skipBeforeTurn = true
  1762. continue
  1763. }
  1764. if recoverIngressPrevResponseNotFound(relayErr, turn, connID) {
  1765. continue
  1766. }
  1767. if retryIngressTurn(relayErr, turn, connID) {
  1768. continue
  1769. }
  1770. finalErr := relayErr
  1771. if unwrapped := errors.Unwrap(relayErr); unwrapped != nil {
  1772. finalErr = unwrapped
  1773. }
  1774. if hooks != nil && hooks.AfterTurn != nil {
  1775. hooks.AfterTurn(turn, nil, finalErr)
  1776. }
  1777. sessionLease.MarkBroken()
  1778. return finalErr
  1779. }
  1780. turnRetry = 0
  1781. turnPrevRecoveryTried = false
  1782. lastTurnFinishedAt = time.Now()
  1783. lastTurnClean = true
  1784. if hooks != nil && hooks.AfterTurn != nil {
  1785. hooks.AfterTurn(turn, result, nil)
  1786. }
  1787. if result == nil {
  1788. return errors.New("websocket turn result is nil")
  1789. }
  1790. responseID := strings.TrimSpace(result.RequestID)
  1791. lastTurnResponseID = responseID
  1792. // 正文共享:currentPayload/currentTurnReplayInput 均不可变,历史直接引用;
  1793. // collector 增量经 combine 合并(新头数组)。
  1794. lastTurnReplayInput = currentTurnReplayInput
  1795. lastTurnReplayInputExists = currentTurnReplayInputExists
  1796. if result.wsReplayInputExists {
  1797. lastTurnReplayInput = combineOpenAIWSReplayItems(lastTurnReplayInput, result.wsReplayInput)
  1798. lastTurnReplayInputExists = true
  1799. }
  1800. nextStrictState, strictStateErr := buildOpenAIWSIngressPreviousTurnStrictState(currentPayload)
  1801. if strictStateErr != nil {
  1802. lastTurnStrictState = nil
  1803. // strict 状态不可用时保留整份上一轮 payload 供慢路径比较。
  1804. lastTurnPayload = currentPayload
  1805. logOpenAIWSModeInfo(
  1806. "ingress_ws_prev_response_strict_state_skip account_id=%d turn=%d conn_id=%s reason=build_error cause=%s",
  1807. account.ID,
  1808. turn,
  1809. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  1810. truncateOpenAIWSLogValue(strictStateErr.Error(), openAIWSLogValueMaxLen),
  1811. )
  1812. } else {
  1813. lastTurnStrictState = nextStrictState
  1814. lastTurnPayload = nil
  1815. }
  1816. if responseID != "" && stateStore != nil {
  1817. ttl := s.openAIWSResponseStickyTTL()
  1818. logOpenAIWSBindResponseAccountWarn(groupID, account.ID, responseID, stateStore.BindResponseAccount(ctx, groupID, responseID, account.ID, ttl))
  1819. stateStore.BindResponseConn(responseID, connID, ttl)
  1820. }
  1821. if stateStore != nil && storeDisabled && sessionHash != "" {
  1822. stateStore.BindSessionConn(groupID, sessionHash, connID, s.openAIWSSessionStickyTTL())
  1823. }
  1824. if connID != "" {
  1825. preferredConnID = connID
  1826. }
  1827. nextClientMessage, readErr := readClientMessage()
  1828. if readErr != nil {
  1829. if isOpenAIWSSessionPreempted(ctx) {
  1830. return errOpenAIWSSessionPreempted
  1831. }
  1832. if isOpenAIWSClientDisconnectError(readErr) {
  1833. closeStatus, closeReason := summarizeOpenAIWSReadCloseError(readErr)
  1834. logOpenAIWSModeInfo(
  1835. "ingress_ws_client_closed account_id=%d conn_id=%s close_status=%s close_reason=%s",
  1836. account.ID,
  1837. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  1838. closeStatus,
  1839. truncateOpenAIWSLogValue(closeReason, openAIWSHeaderValueMaxLen),
  1840. )
  1841. return nil
  1842. }
  1843. return fmt.Errorf("read client websocket request: %w", readErr)
  1844. }
  1845. nextPayload, parseErr := parseClientPayload(turn+1, nextClientMessage)
  1846. if parseErr != nil {
  1847. return parseErr
  1848. }
  1849. nextRoutingFields := gjson.GetManyBytes(nextPayload.payloadRaw, "model", "service_tier")
  1850. if nextPayload.promptCacheKey != "" {
  1851. // ingress 会话在整个客户端 WS 生命周期内复用同一上游连接;
  1852. // prompt_cache_key 对握手头的更新仅在未来需要重新建连时生效。
  1853. updatedHeaders, _, updHdrErr := s.buildOpenAIWSHeaders(
  1854. ctx,
  1855. c,
  1856. account,
  1857. token,
  1858. wsDecision,
  1859. isCodexCLI,
  1860. turnState,
  1861. strings.TrimSpace(c.GetHeader(openAIWSTurnMetadataHeader)),
  1862. nextPayload.promptCacheKey,
  1863. nextRoutingFields[0].String(),
  1864. nextRoutingFields[1].String(),
  1865. )
  1866. if updHdrErr != nil {
  1867. logOpenAIWSModeInfo("ingress_ws_update_headers_failed account_id=%d err=%v", account.ID, updHdrErr)
  1868. } else {
  1869. baseAcquireReq.Headers = updatedHeaders
  1870. }
  1871. }
  1872. setOpenAICodexRoutingHint(baseAcquireReq.Headers, account, nextRoutingFields[0].String(), nextRoutingFields[1].String())
  1873. if nextPayload.previousResponseID != "" {
  1874. expectedPrev := strings.TrimSpace(lastTurnResponseID)
  1875. chainedFromLast := expectedPrev != "" && nextPayload.previousResponseID == expectedPrev
  1876. nextPreviousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(nextPayload.previousResponseID)
  1877. logOpenAIWSModeInfo(
  1878. "ingress_ws_next_turn_chain account_id=%d turn=%d next_turn=%d conn_id=%s previous_response_id=%s previous_response_id_kind=%s last_turn_response_id=%s chained_from_last=%v has_prompt_cache_key=%v store_disabled=%v",
  1879. account.ID,
  1880. turn,
  1881. turn+1,
  1882. truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
  1883. truncateOpenAIWSLogValue(nextPayload.previousResponseID, openAIWSIDValueMaxLen),
  1884. normalizeOpenAIWSLogValue(nextPreviousResponseIDKind),
  1885. truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
  1886. chainedFromLast,
  1887. nextPayload.promptCacheKey != "",
  1888. storeDisabled,
  1889. )
  1890. }
  1891. if stateStore != nil && nextPayload.previousResponseID != "" {
  1892. if stickyConnID, ok := stateStore.GetResponseConn(nextPayload.previousResponseID); ok {
  1893. if sessionConnID != "" && stickyConnID != "" && stickyConnID != sessionConnID {
  1894. logOpenAIWSModeInfo(
  1895. "ingress_ws_keep_session_conn account_id=%d turn=%d conn_id=%s sticky_conn_id=%s previous_response_id=%s",
  1896. account.ID,
  1897. turn,
  1898. truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
  1899. truncateOpenAIWSLogValue(stickyConnID, openAIWSIDValueMaxLen),
  1900. truncateOpenAIWSLogValue(nextPayload.previousResponseID, openAIWSIDValueMaxLen),
  1901. )
  1902. } else {
  1903. preferredConnID = stickyConnID
  1904. }
  1905. }
  1906. }
  1907. currentPayload = nextPayload.payloadRaw
  1908. currentOriginalModel = nextPayload.originalModel
  1909. currentImageBillingModel = nextPayload.imageBillingModel
  1910. currentImageSizeTier = nextPayload.imageSizeTier
  1911. currentImageInputSize = nextPayload.imageInputSize
  1912. currentPayloadBytes = nextPayload.payloadBytes
  1913. currentRequestedReasoningEffort = nextPayload.requestedReasoningEffort
  1914. rejectedFieldRetryState = newOpenAIResponsesRejectedFieldRetryState(currentPayload)
  1915. storeDisabled = s.isOpenAIWSStoreDisabledInRequestRaw(currentPayload, account)
  1916. if !storeDisabled {
  1917. unpinSessionConn(sessionConnID)
  1918. }
  1919. turn++
  1920. }
  1921. }