| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964 |
- package service
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "net/url"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
- coderws "github.com/coder/websocket"
- "github.com/gin-gonic/gin"
- "github.com/tidwall/gjson"
- "github.com/tidwall/sjson"
- )
- type openAIWSRejectedFieldRetryError struct {
- body []byte
- reason string
- }
- func (e *openAIWSRejectedFieldRetryError) Error() string {
- if e == nil || strings.TrimSpace(e.reason) == "" {
- return "retry websocket turn after rejected field normalization"
- }
- return "retry websocket turn after rejected field normalization: " + e.reason
- }
- func openAIWSRejectedFieldRetryHTTPStatus(message []byte) int {
- for _, value := range gjson.GetManyBytes(message, "status", "status_code", "error.status", "error.status_code") {
- status := int(value.Int())
- if status >= 100 && status <= 599 {
- return status
- }
- }
- return openAIWSErrorHTTPStatus(message)
- }
- func (s *OpenAIGatewayService) openAIWSIngressInterTurnIdleTimeout() time.Duration {
- if s == nil || s.cfg == nil || s.cfg.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds <= 0 {
- return 0
- }
- return time.Duration(s.cfg.Gateway.OpenAIWS.IngressInterTurnIdleTimeoutSeconds) * time.Second
- }
- // newOpenAIWSDownstreamWriteContext binds writes directly to the client
- // lifecycle while excluding the separate ingress-lease cancellation signal.
- // This lets a lease-loss path finish its current client write before
- // ReadOpenAIWSClientMessage sends the retryable close frame.
- func newOpenAIWSDownstreamWriteContext(controlCtx context.Context, hooks *OpenAIWSIngressHooks, timeout time.Duration) (context.Context, context.CancelFunc) {
- writeParent := controlCtx
- if hooks != nil && hooks.ClientLifecycleContext != nil {
- writeParent = hooks.ClientLifecycleContext
- }
- if writeParent == nil {
- writeParent = context.Background()
- }
- return context.WithTimeout(writeParent, timeout)
- }
- func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
- ctx context.Context,
- c *gin.Context,
- clientConn *coderws.Conn,
- account *Account,
- token string,
- firstClientMessage []byte,
- hooks *OpenAIWSIngressHooks,
- ) (returnErr error) {
- if s == nil {
- return errors.New("service is nil")
- }
- if c == nil {
- return errors.New("gin context is nil")
- }
- if clientConn == nil {
- return errors.New("client websocket is nil")
- }
- if account == nil {
- return errors.New("account is nil")
- }
- // A handler may reuse the same gin context across account failover attempts.
- // Never let an OAuth attempt's response aliases leak into the next account.
- setCodexToolNameReverse(c, nil)
- if _, err := s.prepareCodexAccountIdentitySource(ctx, c, account); err != nil {
- return err
- }
- if err := validateOpenAIWSBearerToken(account, token); err != nil {
- return err
- }
- // 预取一次 OpenAI Fast Policy settings,绑定到 ctx,让该 WS session
- // 内所有帧的 evaluateOpenAIFastPolicy 调用复用同一份快照,避免每帧
- // 进入 DB / settingRepo。Trade-off 见 withOpenAIFastPolicyContext 注释。
- if s.settingService != nil {
- if settings, err := s.settingService.GetOpenAIFastPolicySettings(ctx); err == nil && settings != nil {
- ctx = withOpenAIFastPolicyContext(ctx, settings)
- }
- }
- // The handler normally owns this registration across retry attempts. Direct
- // callers still get the same session-scoped preemption behavior here.
- if preemptCtx, cleanupPreempt, armed := s.BeginOpenAIWSIngressSessionPreemption(ctx, c, account, firstClientMessage); armed {
- ctx = preemptCtx
- defer cleanupPreempt()
- defer func() {
- if isOpenAIWSSessionPreempted(ctx) {
- returnErr = errOpenAIWSSessionPreempted
- }
- }()
- }
- wsDecision := s.getOpenAIWSProtocolResolver().Resolve(account)
- forceHTTPBridge := account.Platform == PlatformGrok ||
- (s.pluginManager != nil && s.pluginManager.ShouldRouteOpenAIOAuth(account))
- modeRouterV2Enabled := s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.ModeRouterV2Enabled
- ingressMode := OpenAIWSIngressModeCtxPool
- if modeRouterV2Enabled && !forceHTTPBridge {
- ingressMode = account.ResolveOpenAIResponsesWebSocketV2Mode(s.cfg.Gateway.OpenAIWS.IngressModeDefault)
- if ingressMode == OpenAIWSIngressModeOff {
- return NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- "websocket mode is disabled for this account",
- nil,
- )
- }
- switch ingressMode {
- case OpenAIWSIngressModePassthrough:
- if wsDecision.Transport != OpenAIUpstreamTransportResponsesWebsocketV2 {
- return fmt.Errorf("websocket ingress requires ws_v2 transport, got=%s", wsDecision.Transport)
- }
- if s.shouldBridgeOpenAIWSPassthroughFirstMessage(account, firstClientMessage) {
- forceHTTPBridge = true
- break
- }
- // 首轮准入由握手路径完成;后续 response.create 会在写入上游前
- // 依次回调 BeforeRequest 和 BeforeTurn,并在终止或失败时回调
- // AfterTurn,从而覆盖 turn 级利润复核、定价冻结和并发槽位释放。
- return s.proxyResponsesWebSocketV2Passthrough(
- ctx,
- c,
- clientConn,
- account,
- token,
- firstClientMessage,
- hooks,
- wsDecision,
- )
- case OpenAIWSIngressModeHTTPBridge:
- forceHTTPBridge = true
- case OpenAIWSIngressModeCtxPool, OpenAIWSIngressModeShared, OpenAIWSIngressModeDedicated:
- // continue
- default:
- return NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- "websocket mode only supports ctx_pool/passthrough/http_bridge",
- nil,
- )
- }
- }
- if !forceHTTPBridge && wsDecision.Transport != OpenAIUpstreamTransportResponsesWebsocketV2 {
- return fmt.Errorf("websocket ingress requires ws_v2 transport, got=%s", wsDecision.Transport)
- }
- dedicatedMode := modeRouterV2Enabled && ingressMode == OpenAIWSIngressModeDedicated
- wsURL := ""
- wsHost := "-"
- wsPath := "-"
- if forceHTTPBridge {
- wsHost = "xai-http-bridge"
- wsPath = "/v1/responses"
- } else {
- var err error
- wsURL, err = s.buildOpenAIResponsesWSURL(account)
- if err != nil {
- return fmt.Errorf("build ws url: %w", err)
- }
- if parsedURL, parseErr := url.Parse(wsURL); parseErr == nil && parsedURL != nil {
- wsHost = normalizeOpenAIWSLogValue(parsedURL.Host)
- wsPath = normalizeOpenAIWSLogValue(parsedURL.Path)
- }
- }
- debugEnabled := isOpenAIWSModeDebugEnabled()
- isCodexCLI := openai.IsCodexOfficialClientByHeaders(c.GetHeader("User-Agent"), c.GetHeader("originator")) || (s.cfg != nil && s.cfg.Gateway.ForceCodexCLI)
- type openAIWSClientPayload struct {
- payloadRaw []byte
- accountIdentitySourceRaw []byte
- rawForHash []byte
- promptCacheKey string
- previousResponseID string
- originalModel string
- imageBillingModel string
- imageSizeTier string
- imageInputSize string
- payloadBytes int
- requestedReasoningEffort *string
- }
- ingressSessionOriginalModel := ""
- applyPayloadMutation := func(current []byte, path string, value any) ([]byte, error) {
- next, err := sjson.SetBytes(current, path, value)
- if err == nil {
- return next, nil
- }
- // 仅在确实需要修改 payload 且 sjson 失败时,退回 map 路径确保兼容性。
- payload := make(map[string]any)
- if unmarshalErr := json.Unmarshal(current, &payload); unmarshalErr != nil {
- return nil, err
- }
- switch path {
- case "type", "model":
- payload[path] = value
- case "client_metadata." + openAIWSTurnMetadataHeader:
- setOpenAIWSTurnMetadata(payload, fmt.Sprintf("%v", value))
- default:
- return nil, err
- }
- rebuilt, marshalErr := json.Marshal(payload)
- if marshalErr != nil {
- return nil, marshalErr
- }
- return rebuilt, nil
- }
- parseClientPayload := func(turn int, raw []byte) (openAIWSClientPayload, error) {
- trimmed := bytes.TrimSpace(raw)
- if len(trimmed) == 0 {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "empty websocket request payload", nil)
- }
- if !gjson.ValidBytes(trimmed) {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", errors.New("invalid json"))
- }
- values := gjson.GetManyBytes(trimmed, "type", "model", "prompt_cache_key", "previous_response_id")
- eventType := strings.TrimSpace(values[0].String())
- normalized := trimmed
- switch eventType {
- case "":
- eventType = "response.create"
- next, setErr := applyPayloadMutation(normalized, "type", eventType)
- if setErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", setErr)
- }
- normalized = next
- case "response.create":
- case "response.append":
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- "response.append is not supported in ws v2; use response.create with previous_response_id",
- nil,
- )
- default:
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- fmt.Sprintf("unsupported websocket request type: %s", eventType),
- nil,
- )
- }
- requestedReasoningEffort := CanonicalRequestedReasoningEffort(normalized, strings.TrimSpace(values[1].String()))
- if next, policyErr := applyOpenAIWSReasoningEffortPolicy(normalized, hooks); policyErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, policyErr.Error(), policyErr)
- } else {
- normalized = next
- }
- responsesLite := isOpenAIResponsesLiteWebSocketPayload(normalized)
- if compatibilityBody, compatibilityChanged, compatibilityErr := normalizeOpenAIResponsesWebSocketCompatibilityBody(normalized, account, responsesLite); compatibilityErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", compatibilityErr)
- } else if compatibilityChanged {
- normalized = compatibilityBody
- }
- if account.IsOpenAIOAuthLike() {
- aliasedBody, reverse, aliased, aliasErr := aliasOpenAIOAuthReservedToolNamesBody(normalized)
- if aliasErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, aliasErr.Error(), aliasErr)
- }
- updateCodexToolNameReverseForWSFrame(c, normalized, reverse)
- if aliased {
- normalized = aliasedBody
- }
- }
- originalModel := strings.TrimSpace(values[1].String())
- modelMissing := originalModel == ""
- if originalModel == "" {
- // 入站 WS 长会话里,部分客户端只在第一轮 response.create 上声明
- // model,后续 turn 复用同一 session-level model。为避免因省略
- // model 直接断开用户连接,这里回落到上一轮已通过校验的客户端模型,
- // 并在下方写回上游 payload,保证账号模型映射/fast policy/图片权限
- // 仍按同一模型执行。
- originalModel = ingressSessionOriginalModel
- if originalModel == "" {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- "model is required in response.create payload",
- nil,
- )
- }
- }
- promptCacheKey := strings.TrimSpace(values[2].String())
- previousResponseID := strings.TrimSpace(values[3].String())
- previousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(previousResponseID)
- if previousResponseID != "" && previousResponseIDKind == OpenAIPreviousResponseIDKindMessageID {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- "previous_response_id must be a response.id (resp_*), not a message id",
- nil,
- )
- }
- if turnMetadata := strings.TrimSpace(c.GetHeader(openAIWSTurnMetadataHeader)); turnMetadata != "" {
- next, setErr := applyPayloadMutation(normalized, "client_metadata."+openAIWSTurnMetadataHeader, turnMetadata)
- if setErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", setErr)
- }
- normalized = next
- }
- accountIdentitySourceRaw := append([]byte(nil), normalized...)
- accountScopedPayload, accountScoped, scopeErr := applyCodexAccountAndProtectionIdentityRaw(c, account, normalized)
- if scopeErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket identity metadata", scopeErr)
- }
- if accountScoped {
- normalized = accountScopedPayload
- }
- if responsesLite {
- litePayload, _, liteErr := normalizeOpenAIResponsesLitePayloadForAccount(normalized, account)
- if liteErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- liteErr.Error(),
- liteErr,
- )
- }
- normalized = litePayload
- }
- apiKey := getAPIKeyFromContext(c)
- imageGenerationAllowed := GroupAllowsImageGeneration(apiKeyGroup(apiKey))
- codexImageGenerationExplicitToolPolicy := codexImageGenerationExplicitToolPolicyAllow
- if isCodexCLI {
- codexImageGenerationExplicitToolPolicy = account.CodexImageGenerationExplicitToolPolicy()
- }
- codexBridgeEnabled := isCodexCLI &&
- !isOpenAIResponsesLiteWebSocketPayload(normalized) &&
- imageGenerationAllowed &&
- codexImageGenerationExplicitToolPolicy != codexImageGenerationExplicitToolPolicyStrip &&
- s.isCodexImageGenerationBridgeEnabled(ctx, account, apiKey)
- if codexBridgeEnabled {
- payloadMap := make(map[string]any)
- if err := decodeOpenAIJSONUseNumber(normalized, &payloadMap); err != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", err)
- }
- bridgeModified := false
- if ensureOpenAIResponsesImageGenerationTool(payloadMap) {
- bridgeModified = true
- logOpenAIWSModeInfo("ingress_ws_codex_image_tool_injected account_id=%d", account.ID)
- }
- if ensureOpenAIResponsesImageGenerationToolChoiceAuto(payloadMap) {
- bridgeModified = true
- logOpenAIWSModeInfo("ingress_ws_codex_image_tool_choice_auto account_id=%d", account.ID)
- }
- if normalizeOpenAIResponsesImageGenerationTools(payloadMap) {
- bridgeModified = true
- }
- if applyCodexImageGenerationBridgeInstructions(payloadMap) {
- bridgeModified = true
- logOpenAIWSModeInfo("ingress_ws_codex_image_bridge_instructions_added account_id=%d", account.ID)
- }
- if bridgeModified {
- rebuilt, marshalErr := json.Marshal(payloadMap)
- if marshalErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", marshalErr)
- }
- normalized = rebuilt
- }
- }
- requestModel := originalModel
- if hooks != nil && hooks.MapRequestModel != nil {
- mappedModel, mapErr := hooks.MapRequestModel(turn, originalModel)
- if mapErr != nil {
- return openAIWSClientPayload{}, mapErr
- }
- if mappedModel = strings.TrimSpace(mappedModel); mappedModel != "" {
- requestModel = mappedModel
- }
- }
- upstreamModel := normalizeOpenAIModelForUpstream(account, account.GetMappedModel(requestModel))
- if modelMissing || upstreamModel != originalModel {
- next, setErr := applyPayloadMutation(normalized, "model", upstreamModel)
- if setErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", setErr)
- }
- normalized = next
- }
- SetOpsUpstreamModel(c, upstreamModel)
- if isCodexCLI && codexImageGenerationExplicitToolPolicy == codexImageGenerationExplicitToolPolicyStrip {
- if stripped, changed, stripErr := stripOpenAIImageGenerationToolsFromRawPayload(normalized); stripErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", stripErr)
- } else if changed {
- normalized = stripped
- logOpenAIWSModeInfo("ingress_ws_codex_image_tool_stripped_by_policy account_id=%d", account.ID)
- }
- }
- if stripped, changed, stripErr := stripCodexSparkImageGenerationToolFromRawPayload(normalized, upstreamModel); stripErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", stripErr)
- } else if changed {
- normalized = stripped
- logOpenAIWSModeInfo("ingress_ws_codex_spark_image_tool_stripped account_id=%d", account.ID)
- }
- imageIntent := IsImageGenerationIntentForPlatform(openAIResponsesEndpoint, originalModel, normalized, account.Platform)
- if imageIntent && !imageGenerationAllowed {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, ImageGenerationPermissionMessage(), nil)
- }
- imageBillingModel := ""
- imageSizeTier := ""
- imageInputSize := ""
- if imageIntent {
- var imageCfgErr error
- imageCfg, imageCfgErr := resolveOpenAIResponsesImageBillingConfigDetailedFromBody(normalized, originalModel)
- if imageCfgErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, imageCfgErr.Error(), imageCfgErr)
- }
- imageBillingModel = imageCfg.Model
- imageSizeTier = imageCfg.SizeTier
- imageInputSize = imageCfg.InputSize
- }
- // Apply OpenAI Fast Policy on the response.create frame using the same
- // evaluator/normalize/scope rules as the HTTP entrypoints. This is the
- // single integration point for all WS ingress turns (first + follow-up
- // frames flow through here).
- //
- // Model fallback: first turn still requires model at the handler layer;
- // follow-up response.create frames may omit it and then reuse
- // ingressSessionOriginalModel. We always write a concrete upstream model
- // before evaluating policy, so whitelist / filter behavior remains stable.
- policyApplied, blocked, policyErr := s.applyOpenAIFastPolicyToWSResponseCreate(ctx, account, upstreamModel, normalized)
- if policyErr != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, "invalid websocket request payload", policyErr)
- }
- if blocked != nil {
- MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalPolicyDenied)
- // Send a Realtime-style error event to the client first, then
- // signal the handler to close the connection with PolicyViolation.
- // We intentionally do NOT forward this frame upstream.
- //
- // coder/websocket@v1.8.14 Conn.Write is synchronous and flushes
- // the underlying bufio writer before returning (write.go:42 →
- // 307-311), and the subsequent close handshake re-acquires the
- // same writeFrameMu, so the error event is guaranteed to reach
- // the kernel send buffer before any close frame is queued.
- eventBytes := buildOpenAIFastPolicyBlockedWSEvent(blocked)
- if eventBytes != nil {
- writeCtx, cancel := newOpenAIWSDownstreamWriteContext(ctx, hooks, s.openAIWSWriteTimeout())
- _ = clientConn.Write(writeCtx, coderws.MessageText, eventBytes)
- cancel()
- }
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- blocked.Message,
- blocked,
- )
- }
- normalized = policyApplied
- ingressSessionOriginalModel = originalModel
- if account.RequestIntegrityMode() != "off" {
- if err := checkAccountRequestIntegrity(c, account, trimmed, normalized); err != nil {
- return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, err.Error(), err)
- }
- stageMode1Request(c, account, trimmed)
- }
- return openAIWSClientPayload{
- payloadRaw: normalized,
- accountIdentitySourceRaw: accountIdentitySourceRaw,
- rawForHash: trimmed,
- promptCacheKey: promptCacheKey,
- previousResponseID: previousResponseID,
- originalModel: originalModel,
- imageBillingModel: imageBillingModel,
- imageSizeTier: imageSizeTier,
- imageInputSize: imageInputSize,
- payloadBytes: len(normalized),
- requestedReasoningEffort: requestedReasoningEffort,
- }, nil
- }
- writeClientMessage := func(message []byte) error {
- writeCtx, cancel := newOpenAIWSDownstreamWriteContext(ctx, hooks, s.openAIWSWriteTimeout())
- defer cancel()
- message = restoreCodexToolNamesFromContext(c, message)
- return clientConn.Write(writeCtx, coderws.MessageText, message)
- }
- readClientMessage := func() ([]byte, error) {
- idleTimeout := s.openAIWSIngressInterTurnIdleTimeout()
- msgType, payload, readErr := ReadOpenAIWSClientMessage(
- ctx,
- clientConn,
- idleTimeout,
- coderws.StatusNormalClosure,
- "websocket idle timeout",
- )
- if readErr != nil {
- var closeErr *OpenAIWSClientCloseError
- if errors.As(readErr, &closeErr) && closeErr.StatusCode() == coderws.StatusNormalClosure {
- logOpenAIWSModeInfo("ingress_ws_inter_turn_idle_timeout account_id=%d timeout_seconds=%d", account.ID, int(idleTimeout.Seconds()))
- }
- return nil, readErr
- }
- if msgType != coderws.MessageText && msgType != coderws.MessageBinary {
- return nil, NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- fmt.Sprintf("unsupported websocket client message type: %s", msgType.String()),
- nil,
- )
- }
- return payload, nil
- }
- firstPayload, err := parseClientPayload(1, firstClientMessage)
- if err != nil {
- return err
- }
- useHTTPBridge := forceHTTPBridge || s.shouldBridgeOpenAIWSHTTP(account, firstPayload.payloadBytes, firstPayload.previousResponseID)
- turnState := strings.TrimSpace(c.GetHeader(openAIWSTurnStateHeader))
- stateStore := s.getOpenAIWSStateStore()
- groupID := getOpenAIGroupIDFromContext(c)
- storeDisabledConnMode := s.openAIWSStoreDisabledConnMode()
- sessionHash := ""
- preferredConnID := ""
- storeDisabled := false
- refreshIngressRouteState := func(payload openAIWSClientPayload) {
- sessionHash = s.GenerateSessionHash(c, payload.rawForHash)
- preferredConnID = ""
- storeDisabled = s.isOpenAIWSStoreDisabledInRequestRaw(payload.payloadRaw, account)
- if useHTTPBridge {
- // Sticky account affinity may be shared, but an HTTP bridge must not
- // inherit another connection's native WS turn state or socket binding.
- return
- }
- if turnState == "" && stateStore != nil && sessionHash != "" {
- if savedTurnState, ok := stateStore.GetSessionTurnState(groupID, sessionHash); ok {
- turnState = savedTurnState
- }
- }
- if stateStore != nil && payload.previousResponseID != "" {
- if connID, ok := stateStore.GetResponseConn(payload.previousResponseID); ok {
- preferredConnID = connID
- }
- }
- if stateStore != nil && storeDisabled && payload.previousResponseID == "" && sessionHash != "" {
- if connID, ok := stateStore.GetSessionConn(groupID, sessionHash); ok {
- preferredConnID = connID
- }
- }
- }
- refreshIngressRouteState(firstPayload)
- if useHTTPBridge {
- logOpenAIWSModeInfo(
- "ingress_ws_http_bridge_start account_id=%d account_type=%s payload_bytes=%d threshold_bytes=%d has_session_hash=%v store_disabled=%v",
- account.ID,
- account.Type,
- firstPayload.payloadBytes,
- s.openAIWSHTTPBridgeThresholdBytes(),
- sessionHash != "",
- storeDisabled,
- )
- currentBridgePayload := firstPayload
- // Keep the first turn as the stable conversation seed. The mapped model
- // is resolved again for each turn below so an in-connection model switch
- // cannot reuse another model's upstream cache identity.
- grokCacheSeedPayload := firstPayload.payloadRaw
- var bridgeReplayInput []json.RawMessage
- bridgeReplayInputExists := false
- var bridgeAccountFailoverInput []json.RawMessage
- bridgeAccountFailoverInputExists := false
- for turn := 1; ; turn++ {
- if turn > 1 && hooks != nil && hooks.BeforeRequest != nil {
- if err := hooks.BeforeRequest(turn, currentBridgePayload.payloadRaw, currentBridgePayload.originalModel); err != nil {
- return err
- }
- }
- if hooks != nil && hooks.BeforeTurn != nil {
- if err := hooks.BeforeTurn(turn); err != nil {
- return err
- }
- }
- if turnState != "" && c != nil && c.Request != nil {
- c.Request.Header.Set(openAIWSTurnStateHeader, turnState)
- }
- if c != nil && sessionHash != "" {
- c.Set(openAIWSIngressSessionHashContextKey, sessionHash)
- }
- // 剥离本会话已知失效的加密项,阻断同一失效密文随历史反复触发上游拒绝。
- // 历史序列须同步剥离,否则与已剥离的当前 input 项错位,prefix 复用失配。
- if invalidDigests := s.sessionInvalidEncryptedContentDigests(groupID, sessionHash); len(invalidDigests) > 0 {
- strippedPayload, strippedCount := s.stripSessionInvalidEncryptedContentLogged(
- currentBridgePayload.payloadRaw, invalidDigests, "ingress_ws_http_bridge_invalid_encrypted_lineage_strip", account.ID, turn,
- )
- if strippedCount > 0 {
- currentBridgePayload.payloadRaw = strippedPayload
- currentBridgePayload.payloadBytes = len(strippedPayload)
- }
- if bridgeReplayInputExists {
- bridgeReplayInput, _ = stripOpenAIInvalidEncryptedContentFromReplayItems(bridgeReplayInput, invalidDigests)
- }
- if bridgeAccountFailoverInputExists {
- bridgeAccountFailoverInput, _ = stripOpenAIInvalidEncryptedContentFromReplayItems(bridgeAccountFailoverInput, invalidDigests)
- }
- }
- bridgePayloadRaw := currentBridgePayload.payloadRaw
- bridgePayloadBytes := currentBridgePayload.payloadBytes
- toolOutputCoverage := AnalyzeToolCallOutputContextCoverageBytes(currentBridgePayload.payloadRaw)
- needsBridgeReplay := currentBridgePayload.previousResponseID != "" ||
- (toolOutputCoverage.HasFunctionCallOutput && !toolOutputCoverage.ContextCoversAllCallIDs)
- // 一次解析当前 input,正常 replay 与 account-failover 两份序列共享同一批正文。
- bridgeCurrentItems, bridgeCurrentItemsExist, extractErr := openAIWSExtractNormalizedInputSequence(
- currentBridgePayload.payloadRaw,
- )
- if extractErr != nil {
- return fmt.Errorf("build websocket http bridge replay input: %w", extractErr)
- }
- turnReplayInput, turnReplayInputExists := buildOpenAIWSReplayInputSequenceFromItems(
- bridgeReplayInput,
- bridgeReplayInputExists,
- bridgeCurrentItems,
- bridgeCurrentItemsExist,
- needsBridgeReplay,
- )
- turnAccountFailoverInput, turnAccountFailoverInputExists := buildOpenAIWSReplayInputSequenceFromItems(
- bridgeAccountFailoverInput,
- bridgeAccountFailoverInputExists,
- bridgeCurrentItems,
- bridgeCurrentItemsExist,
- needsBridgeReplay,
- )
- if needsBridgeReplay && turnReplayInputExists {
- updatedPayload, setInputErr := setOpenAIWSPayloadInputSequence(
- currentBridgePayload.payloadRaw,
- turnReplayInput,
- true,
- )
- if setInputErr != nil {
- return fmt.Errorf("set websocket http bridge replay input: %w", setInputErr)
- }
- bridgePayloadRaw = updatedPayload
- bridgePayloadBytes = len(updatedPayload)
- logOpenAIWSModeInfo(
- "ingress_ws_http_bridge_replay_input account_id=%d turn=%d input_items=%d previous_response_id_present=%v has_tool_output=%v",
- account.ID,
- turn,
- len(turnReplayInput),
- currentBridgePayload.previousResponseID != "",
- openAIWSRawPayloadHasToolCallOutput(currentBridgePayload.payloadRaw),
- )
- }
- grokCacheIdentity := ""
- if account.Platform == PlatformGrok {
- grokCacheIdentity, err = resolveGrokWSCacheIdentity(
- c,
- account,
- grokCacheSeedPayload,
- currentBridgePayload.payloadRaw,
- currentBridgePayload.originalModel,
- )
- if err != nil {
- return fmt.Errorf("resolve Grok websocket cache identity: %w", err)
- }
- }
- if err := validateMode1StagedRequest(c, account, bridgePayloadRaw); err != nil {
- return err
- }
- result, bridgeErr := s.proxyOpenAIWSHTTPBridgeTurn(
- ctx,
- c,
- account,
- token,
- bridgePayloadRaw,
- bridgePayloadBytes,
- currentBridgePayload.originalModel,
- currentBridgePayload.imageBillingModel,
- currentBridgePayload.imageSizeTier,
- currentBridgePayload.imageInputSize,
- grokCacheIdentity,
- turn,
- writeClientMessage,
- )
- if bridgeErr != nil && isOpenAIWSSessionPreempted(ctx) {
- return errOpenAIWSSessionPreempted
- }
- if hooks != nil && hooks.AfterTurn != nil {
- hooks.AfterTurn(turn, result, bridgeErr)
- }
- if bridgeErr != nil {
- var failoverErr *UpstreamFailoverError
- if turn > 1 && errors.As(bridgeErr, &failoverErr) && failoverErr != nil {
- retryPayload, retrySafe, retryPayloadErr := buildOpenAIWSCurrentTurnRetryPayload(
- currentBridgePayload.accountIdentitySourceRaw,
- turnAccountFailoverInput,
- turnAccountFailoverInputExists,
- currentBridgePayload.originalModel,
- )
- if retryPayloadErr != nil {
- return fmt.Errorf("build websocket current-turn failover payload: %w", retryPayloadErr)
- }
- if !retrySafe {
- retryPayload = nil
- }
- return newOpenAIWSCurrentTurnFailoverError(bridgeErr, retryPayload)
- }
- return bridgeErr
- }
- if result == nil {
- return errors.New("websocket http bridge turn result is nil")
- }
- // turnReplayInput/turnAccountFailoverInput 可能共享同一头数组(转移自
- // bridgeCurrentItems),保存历史必须经 combine 新建头,禁止就地 append。
- bridgeReplayInput = turnReplayInput
- bridgeReplayInputExists = turnReplayInputExists
- if result.wsReplayInputExists {
- bridgeReplayInput = combineOpenAIWSReplayItems(bridgeReplayInput, result.wsReplayInput)
- bridgeReplayInputExists = true
- }
- bridgeAccountFailoverInput = turnAccountFailoverInput
- bridgeAccountFailoverInputExists = turnAccountFailoverInputExists
- if len(result.wsAccountFailoverReplayInput) > 0 {
- bridgeAccountFailoverInput = combineOpenAIWSReplayItems(
- bridgeAccountFailoverInput,
- result.wsAccountFailoverReplayInput,
- )
- bridgeAccountFailoverInputExists = true
- }
- if bridgeTurnState := strings.TrimSpace(result.ResponseHeaders.Get(openAIWSTurnStateHeader)); bridgeTurnState != "" {
- // Follow-up turns on this bridge retain their own upstream state;
- // publishing it by session hash would leak it to independent bridges.
- turnState = bridgeTurnState
- }
- responseID := strings.TrimSpace(result.RequestID)
- if responseID != "" && stateStore != nil {
- ttl := s.openAIWSResponseStickyTTL()
- logOpenAIWSBindResponseAccountWarn(groupID, account.ID, responseID, stateStore.BindResponseAccount(ctx, groupID, responseID, account.ID, ttl))
- }
- nextClientMessage, readErr := readClientMessage()
- if readErr != nil {
- if isOpenAIWSSessionPreempted(ctx) {
- return errOpenAIWSSessionPreempted
- }
- if isOpenAIWSClientDisconnectError(readErr) {
- closeStatus, closeReason := summarizeOpenAIWSReadCloseError(readErr)
- logOpenAIWSModeInfo(
- "ingress_ws_http_bridge_client_closed account_id=%d close_status=%s close_reason=%s",
- account.ID,
- closeStatus,
- truncateOpenAIWSLogValue(closeReason, openAIWSHeaderValueMaxLen),
- )
- return nil
- }
- return fmt.Errorf("read client websocket request: %w", readErr)
- }
- nextPayload, parseErr := parseClientPayload(turn+1, nextClientMessage)
- if parseErr != nil {
- return parseErr
- }
- currentBridgePayload = nextPayload
- }
- }
- firstRoutingFields := gjson.GetManyBytes(firstPayload.payloadRaw, "model", "service_tier")
- wsHeaders, _, buildHdrErr := s.buildOpenAIWSHeaders(
- ctx,
- c,
- account,
- token,
- wsDecision,
- isCodexCLI,
- turnState,
- strings.TrimSpace(c.GetHeader(openAIWSTurnMetadataHeader)),
- firstPayload.promptCacheKey,
- firstRoutingFields[0].String(),
- firstRoutingFields[1].String(),
- )
- if buildHdrErr != nil {
- return fmt.Errorf("build ws headers: %w", buildHdrErr)
- }
- baseAcquireReq := openAIWSAcquireRequest{
- Account: account,
- WSURL: wsURL,
- Headers: wsHeaders,
- HeadersFactory: func(factoryCtx context.Context, headers http.Header) (http.Header, error) {
- return s.refreshOpenAIAgentIdentityHeaders(factoryCtx, account, headers)
- },
- ProxyURL: func() string {
- if account.ProxyID != nil && account.Proxy != nil {
- return account.Proxy.URL()
- }
- return ""
- }(),
- ForceNewConn: false,
- }
- pool := s.getOpenAIWSConnPool()
- if pool == nil {
- return errors.New("openai ws conn pool is nil")
- }
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- account.Type,
- normalizeOpenAIWSLogValue(string(wsDecision.Transport)),
- wsHost,
- wsPath,
- normalizeOpenAIWSLogValue(ingressMode),
- storeDisabled,
- sessionHash != "",
- firstPayload.previousResponseID != "",
- )
- if debugEnabled {
- logOpenAIWSModeDebug(
- "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",
- account.ID,
- account.Type,
- normalizeOpenAIWSLogValue(string(wsDecision.Transport)),
- wsHost,
- truncateOpenAIWSLogValue(preferredConnID, openAIWSIDValueMaxLen),
- sessionHash != "",
- firstPayload.previousResponseID != "",
- storeDisabled,
- )
- }
- if firstPayload.previousResponseID != "" {
- firstPreviousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(firstPayload.previousResponseID)
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- 1,
- truncateOpenAIWSLogValue(firstPayload.previousResponseID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(firstPreviousResponseIDKind),
- truncateOpenAIWSLogValue(preferredConnID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(sessionHash, 12),
- openAIWSHeaderValueForLog(baseAcquireReq.Headers, "session_id"),
- openAIWSHeaderValueForLog(baseAcquireReq.Headers, "conversation_id"),
- turnState != "",
- len(turnState),
- firstPayload.promptCacheKey != "",
- storeDisabled,
- )
- }
- acquireTimeout := s.openAIWSAcquireTimeout()
- if acquireTimeout <= 0 {
- acquireTimeout = 30 * time.Second
- }
- agentTaskRecoveryTried := false
- var acquireTurnLease func(int, string, bool) (*openAIWSConnLease, error)
- acquireTurnLease = func(turn int, preferred string, forcePreferredConn bool) (*openAIWSConnLease, error) {
- req := cloneOpenAIWSAcquireRequest(baseAcquireReq)
- req.PreferredConnID = strings.TrimSpace(preferred)
- req.ForcePreferredConn = forcePreferredConn
- // dedicated 模式下每次获取均新建连接,避免跨会话复用残留上下文。
- req.ForceNewConn = dedicatedMode
- acquireCtx, acquireCancel := context.WithTimeout(ctx, acquireTimeout)
- lease, acquireErr := pool.Acquire(acquireCtx, req)
- acquireCancel()
- var dialErr *openAIWSDialError
- if acquireErr != nil && s.isAgentIdentityAccount(ctx, account) && errors.As(acquireErr, &dialErr) && isAgentIdentityTaskInvalidWSDialError(dialErr) && !agentTaskRecoveryTried {
- agentTaskRecoveryTried = true
- if recoveryErr := s.recoverAgentIdentityTask(ctx, account, account.GetCredential("task_id")); recoveryErr != nil {
- return nil, fmt.Errorf("agent identity task recovery failed: %w", recoveryErr)
- }
- return acquireTurnLease(turn, preferred, forcePreferredConn)
- }
- if acquireErr != nil {
- if isOpenAIWSSessionPreempted(ctx) {
- return nil, errOpenAIWSSessionPreempted
- }
- canonicalModel := canonicalOpenAIAccountSchedulingModel(account, ingressSessionOriginalModel)
- s.handleOpenAIWSDialTransientFailure(ctx, account, canonicalModel, acquireErr)
- dialStatus, dialClass, dialCloseStatus, dialCloseReason, dialRespServer, dialRespVia, dialRespCFRay, dialRespReqID := summarizeOpenAIWSDialError(acquireErr)
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- normalizeOpenAIWSLogValue(classifyOpenAIWSAcquireError(acquireErr)),
- dialStatus,
- dialClass,
- dialCloseStatus,
- truncateOpenAIWSLogValue(dialCloseReason, openAIWSHeaderValueMaxLen),
- dialRespServer,
- dialRespVia,
- dialRespCFRay,
- dialRespReqID,
- truncateOpenAIWSLogValue(acquireErr.Error(), openAIWSLogValueMaxLen),
- truncateOpenAIWSLogValue(preferred, openAIWSIDValueMaxLen),
- forcePreferredConn,
- wsHost,
- wsPath,
- account.ProxyID != nil && account.Proxy != nil,
- )
- var dialErr *openAIWSDialError
- if errors.As(acquireErr, &dialErr) && dialErr != nil && dialErr.StatusCode == http.StatusTooManyRequests {
- s.persistOpenAIWSRateLimitSignal(ctx, account, dialErr.ResponseHeaders, nil, "rate_limit_exceeded", "rate_limit_error", strings.TrimSpace(acquireErr.Error()), canonicalModel)
- return nil, s.newOpenAIWSRateLimitFailoverError(account, dialErr.ResponseHeaders, nil, acquireErr.Error())
- }
- if errors.Is(acquireErr, errOpenAIWSPreferredConnUnavailable) {
- return nil, NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- "upstream continuation connection is unavailable; please restart the conversation",
- acquireErr,
- )
- }
- if errors.Is(acquireErr, context.DeadlineExceeded) || errors.Is(acquireErr, errOpenAIWSConnQueueFull) {
- return nil, NewOpenAIWSClientCloseError(
- coderws.StatusTryAgainLater,
- "upstream websocket is busy, please retry later",
- acquireErr,
- )
- }
- return nil, acquireErr
- }
- connID := strings.TrimSpace(lease.ConnID())
- if handshakeTurnState := strings.TrimSpace(lease.HandshakeHeader(openAIWSTurnStateHeader)); handshakeTurnState != "" {
- turnState = handshakeTurnState
- if stateStore != nil && sessionHash != "" {
- stateStore.BindSessionTurnState(groupID, sessionHash, handshakeTurnState, s.openAIWSSessionStickyTTL())
- }
- updatedHeaders := cloneHeader(baseAcquireReq.Headers)
- if updatedHeaders == nil {
- updatedHeaders = make(http.Header)
- }
- updatedHeaders.Set(openAIWSTurnStateHeader, handshakeTurnState)
- baseAcquireReq.Headers = updatedHeaders
- }
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- lease.Reused(),
- lease.ConnPickDuration().Milliseconds(),
- lease.QueueWaitDuration().Milliseconds(),
- truncateOpenAIWSLogValue(preferred, openAIWSIDValueMaxLen),
- )
- return lease, nil
- }
- var rejectedFieldRetryState *openAIResponsesRejectedFieldRetryState
- sendAndRelay := func(turn int, lease *openAIWSConnLease, payload []byte, payloadBytes int, originalModel string, imageBillingModel string, imageSizeTier string, imageInputSize string, requestedReasoningEffort *string) (trafficResult *OpenAIForwardResult, trafficErr error) {
- ctx, permit, admissionErr := beginAccountTrafficTurn(ctx, s.httpUpstream, account)
- if admissionErr != nil {
- return nil, admissionErr
- }
- defer func() { finishAccountTrafficTurn(permit, trafficErr) }()
- responseModelObserver := &upstreamResponseModelObserver{}
- if lease == nil {
- return nil, errors.New("upstream websocket lease is nil")
- }
- turnStart := time.Now()
- wroteDownstream := false
- if err := validateMode1StagedRequest(c, account, payload); err != nil {
- return nil, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, err.Error(), err)
- }
- if err := lease.WriteJSONWithContextTimeout(ctx, json.RawMessage(payload), s.openAIWSWriteTimeout()); err != nil {
- return nil, wrapOpenAIWSIngressTurnError(
- "write_upstream",
- fmt.Errorf("write upstream websocket request: %w", err),
- false,
- )
- }
- if debugEnabled {
- logOpenAIWSModeDebug(
- "ingress_ws_turn_request_sent account_id=%d turn=%d conn_id=%s payload_bytes=%d",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
- payloadBytes,
- )
- }
- responseID := ""
- usage := OpenAIUsage{}
- imageCounter := newOpenAIImageOutputCounter()
- var firstTokenMs *int
- reqStream := openAIWSPayloadBoolFromRaw(payload, "stream", true)
- turnPreviousResponseID := openAIWSPayloadStringFromRaw(payload, "previous_response_id")
- turnPreviousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(turnPreviousResponseID)
- turnPromptCacheKey := openAIWSPayloadStringFromRaw(payload, "prompt_cache_key")
- turnStoreDisabled := s.isOpenAIWSStoreDisabledInRequestRaw(payload, account)
- turnHasFunctionCallOutput := openAIWSRawPayloadHasToolCallOutput(payload)
- eventCount := 0
- tokenEventCount := 0
- terminalEventCount := 0
- replayCollector := &openAIWSToolCallReplayCollector{}
- firstEventType := ""
- lastEventType := ""
- needModelReplace := false
- clientDisconnected := false
- mappedModel := ""
- var mappedModelBytes []byte
- if originalModel != "" {
- mappedModel = strings.TrimSpace(gjson.GetBytes(payload, "model").String())
- if mappedModel == "" {
- mappedModel = normalizeOpenAIModelForUpstream(account, account.GetMappedModel(originalModel))
- }
- needModelReplace = mappedModel != "" && mappedModel != originalModel
- if needModelReplace {
- mappedModelBytes = []byte(mappedModel)
- }
- }
- for {
- upstreamMessage, readErr := lease.ReadMessageWithContextTimeout(ctx, s.openAIWSReadTimeout())
- if readErr != nil {
- lease.MarkBroken()
- return nil, wrapOpenAIWSIngressTurnError(
- "read_upstream",
- fmt.Errorf("read upstream websocket event: %w", readErr),
- wroteDownstream,
- )
- }
- if normalized, changed := normalizeCompletedImageGenerationStatus(upstreamMessage); changed {
- upstreamMessage = normalized
- }
- eventType, eventResponseID, _ := parseOpenAIWSEventEnvelope(upstreamMessage)
- responseModelObserver.ObserveOpenAI(upstreamMessage, eventType)
- if responseID == "" && eventResponseID != "" {
- responseID = eventResponseID
- }
- if eventType != "" {
- eventCount++
- if firstEventType == "" {
- firstEventType = eventType
- }
- lastEventType = eventType
- }
- if openAIWSMessageShouldParseUsage(eventType, upstreamMessage) {
- parseOpenAIWSResponseUsageFromCompletedEvent(upstreamMessage, &usage)
- }
- if eventType == "error" || eventType == "response.failed" {
- markOpenAICyberPolicyEvent(c, upstreamMessage, http.StatusOK, &usage)
- }
- if eventType == "error" {
- s.handleOpenAIWSErrorEventTransientFailure(ctx, account, mappedModel, lease.HandshakeHeaders(), upstreamMessage)
- errCodeRaw, errTypeRaw, errMsgRaw := parseOpenAIWSErrorEventFields(upstreamMessage)
- statusCode := openAIWSRejectedFieldRetryHTTPStatus(upstreamMessage)
- if !wroteDownstream && statusCode == http.StatusBadRequest && rejectedFieldRetryState != nil {
- retryBody, retryReason, changed, retryErr := normalizeOpenAIResponsesRejectedFieldRetryBody(
- statusCode,
- payload,
- upstreamMessage,
- )
- if retryErr != nil {
- return nil, fmt.Errorf("normalize websocket rejected field retry: %w", retryErr)
- }
- if changed && rejectedFieldRetryState.Allow(retryBody) {
- logOpenAIWSModeInfo(
- "ingress_ws_rejected_field_retry account_id=%d turn=%d conn_id=%s reason=%s",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(retryReason, openAIWSLogValueMaxLen),
- )
- return nil, &openAIWSRejectedFieldRetryError{
- body: append([]byte(nil), retryBody...),
- reason: retryReason,
- }
- }
- }
- s.persistOpenAIWSRateLimitSignal(ctx, account, lease.HandshakeHeaders(), upstreamMessage, errCodeRaw, errTypeRaw, errMsgRaw, mappedModel)
- fallbackReason, _ := classifyOpenAIWSErrorEventFromRaw(errCodeRaw, errTypeRaw, errMsgRaw)
- if fallbackReason == openAIWSFallbackReasonInvalidEncryptedContent {
- // 记录被上游拒绝的密文摘要;错误照旧透传,下一轮进场时按摘要预剥离。
- if digests := collectOpenAIEncryptedContentDigestsRaw(payload); len(digests) > 0 {
- s.markOpenAIWSInvalidEncryptedContentLineage(groupID, sessionHash, digests)
- logOpenAIWSModeInfo(
- "ingress_ws_invalid_encrypted_lineage_mark account_id=%d turn=%d digests=%d",
- account.ID,
- turn,
- len(digests),
- )
- }
- }
- errCode, errType, errMessage := summarizeOpenAIWSErrorEventFieldsFromRaw(errCodeRaw, errTypeRaw, errMsgRaw)
- recoverablePrevNotFound := fallbackReason == openAIWSIngressStagePreviousResponseNotFound &&
- turnPreviousResponseID != "" &&
- !turnHasFunctionCallOutput &&
- s.openAIWSIngressPreviousResponseRecoveryEnabled() &&
- !wroteDownstream
- if recoverablePrevNotFound {
- // 可恢复场景使用非 error 关键字日志,避免被 LegacyPrintf 误判为 ERROR 级别。
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
- eventCount,
- truncateOpenAIWSLogValue(fallbackReason, openAIWSLogValueMaxLen),
- errCode,
- errType,
- errMessage,
- truncateOpenAIWSLogValue(turnPreviousResponseID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(turnPreviousResponseIDKind),
- truncateOpenAIWSLogValue(responseID, openAIWSIDValueMaxLen),
- turnStoreDisabled,
- turnPromptCacheKey != "",
- )
- } else {
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
- eventCount,
- truncateOpenAIWSLogValue(fallbackReason, openAIWSLogValueMaxLen),
- errCode,
- errType,
- errMessage,
- truncateOpenAIWSLogValue(turnPreviousResponseID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(turnPreviousResponseIDKind),
- truncateOpenAIWSLogValue(responseID, openAIWSIDValueMaxLen),
- turnStoreDisabled,
- turnPromptCacheKey != "",
- )
- }
- // previous_response_not_found 在 ingress 模式支持单次恢复重试:
- // 不把该 error 直接下发客户端,而是由上层去掉 previous_response_id 后重放当前 turn。
- if recoverablePrevNotFound {
- lease.MarkBroken()
- errMsg := strings.TrimSpace(errMsgRaw)
- if errMsg == "" {
- errMsg = "previous response not found"
- }
- return nil, wrapOpenAIWSIngressTurnError(
- openAIWSIngressStagePreviousResponseNotFound,
- errors.New(errMsg),
- false,
- )
- }
- if !wroteDownstream && isOpenAIWSRateLimitError(errCodeRaw, errTypeRaw, errMsgRaw) {
- lease.MarkBroken()
- return nil, s.newOpenAIWSRateLimitFailoverError(account, lease.HandshakeHeaders(), upstreamMessage, errMsgRaw)
- }
- }
- isTokenEvent := isOpenAIWSTokenEvent(eventType)
- if isTokenEvent {
- tokenEventCount++
- }
- isTerminalEvent := isOpenAIWSTerminalEvent(eventType)
- if isTerminalEvent {
- terminalEventCount++
- }
- if firstTokenMs == nil && isTokenEvent {
- ms := int(time.Since(turnStart).Milliseconds())
- firstTokenMs = &ms
- }
- imageCounter.AddSSEData(upstreamMessage)
- if !clientDisconnected {
- if needModelReplace && len(mappedModelBytes) > 0 && openAIWSEventMayContainModel(eventType) && bytes.Contains(upstreamMessage, mappedModelBytes) {
- upstreamMessage = replaceOpenAIWSMessageModel(upstreamMessage, mappedModel, originalModel)
- }
- if openAIWSEventMayContainToolCalls(eventType) && openAIWSMessageLikelyContainsToolCalls(upstreamMessage) {
- if corrected, changed := s.toolCorrector.CorrectToolCallsInSSEBytes(upstreamMessage); changed {
- upstreamMessage = corrected
- }
- }
- replayCollector.AddEvent(eventType, upstreamMessage)
- // 客户端写出副本改写容量降载码:Codex 对 error/response.failed 中的
- // server_is_overloaded / slow_down 判致命并终止会话,改写后走客户端
- // 内置退避重试。HTTP/SSE(openai_gateway_response_handling.go)与
- // http_bridge(openai_ws_http_bridge.go)两条路径早已这么做,
- // ctx_pool 的 ingress 直写路径是唯一漏掉的一条 —— 同一个上游降载
- // 事件在这里会让会话就地终止,切到 http_bridge 却能正常退避重试。
- //
- // 必须写进独立变量而不是原地改 upstreamMessage:下面的
- // markOpenAIWSClientVisibleFailure 与 handleOpenAIWSTerminalTransientFailure
- // 仍要按未改写的原始 payload 判定账号状态,这正是
- // sanitizeOpenAICapacityShedErrorCodeForClient 注释里写明的前提。
- clientMessage := upstreamMessage
- if eventType == "error" || eventType == "response.failed" {
- if rewritten, changed := sanitizeOpenAICapacityShedErrorCodeForClient(clientMessage); changed {
- clientMessage = rewritten
- }
- }
- if err := writeClientMessage(clientMessage); err != nil {
- if isOpenAIWSClientDisconnectError(err) {
- clientDisconnected = true
- closeStatus, closeReason := summarizeOpenAIWSReadCloseError(err)
- logOpenAIWSModeInfo(
- "ingress_ws_client_disconnected_drain account_id=%d turn=%d conn_id=%s close_status=%s close_reason=%s",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
- closeStatus,
- truncateOpenAIWSLogValue(closeReason, openAIWSHeaderValueMaxLen),
- )
- } else {
- return nil, wrapOpenAIWSIngressTurnError(
- "write_client",
- fmt.Errorf("write client websocket event: %w", err),
- wroteDownstream,
- )
- }
- } else {
- wroteDownstream = true
- markOpenAIWSClientVisibleFailure(c, eventType, upstreamMessage)
- }
- }
- if isTerminalEvent {
- terminalEvent := s.handleOpenAIWSTerminalTransientFailure(ctx, account, mappedModel, lease.HandshakeHeaders(), upstreamMessage)
- // 客户端已断连时,上游连接的 session 状态不可信,标记 broken 避免回池复用。
- if clientDisconnected {
- lease.MarkBroken()
- }
- firstTokenMsValue := -1
- if firstTokenMs != nil {
- firstTokenMsValue = *firstTokenMs
- }
- if debugEnabled {
- logOpenAIWSModeDebug(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(lease.ConnID(), openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(responseID, openAIWSIDValueMaxLen),
- time.Since(turnStart).Milliseconds(),
- eventCount,
- tokenEventCount,
- terminalEventCount,
- truncateOpenAIWSLogValue(firstEventType, openAIWSLogValueMaxLen),
- truncateOpenAIWSLogValue(lastEventType, openAIWSLogValueMaxLen),
- firstTokenMsValue,
- clientDisconnected,
- )
- }
- imageCount := imageCounter.Count()
- result := &OpenAIForwardResult{
- RequestID: responseID,
- Usage: usage,
- Model: originalModel,
- UpstreamModel: mappedModel,
- UpstreamResponseModel: responseModelObserver.Model(),
- UpstreamResponseModelConflict: responseModelObserver.Conflict(),
- UpstreamResponseServiceTier: responseModelObserver.ServiceTier(),
- ServiceTier: resolvedOpenAIUpstreamServiceTierFromObserver(responseModelObserver, extractOpenAIServiceTierFromBody(payload)),
- ReasoningEffort: ApplyThinkingEnabledFallback(extractOpenAIReasoningEffortFromBody(payload, mappedModel, originalModel), payload, mappedModel),
- RequestedReasoningEffort: requestedReasoningEffort,
- Stream: reqStream,
- OpenAIWSMode: true,
- UpstreamTerminalEvent: terminalEvent,
- ResponseHeaders: lease.HandshakeHeaders(),
- Duration: time.Since(turnStart),
- FirstTokenMs: firstTokenMs,
- }
- if replayInput := replayCollector.Items(); len(replayInput) > 0 {
- result.wsReplayInput = replayInput
- result.wsReplayInputExists = true
- }
- if imageCount > 0 {
- result.ImageCount = imageCount
- result.ImageSize = imageSizeTier
- result.ImageInputSize = imageInputSize
- result.ImageOutputSizes = imageCounter.Sizes()
- result.BillingModel = imageBillingModel
- }
- return result, nil
- }
- }
- }
- currentPayload := firstPayload.payloadRaw
- currentOriginalModel := firstPayload.originalModel
- currentImageBillingModel := firstPayload.imageBillingModel
- currentImageSizeTier := firstPayload.imageSizeTier
- currentImageInputSize := firstPayload.imageInputSize
- currentPayloadBytes := firstPayload.payloadBytes
- currentRequestedReasoningEffort := firstPayload.requestedReasoningEffort
- isStrictAffinityTurn := func(payload []byte) bool {
- if !storeDisabled {
- return false
- }
- return strings.TrimSpace(openAIWSPayloadStringFromRaw(payload, "previous_response_id")) != ""
- }
- var sessionLease *openAIWSConnLease
- sessionConnID := ""
- pinnedSessionConnID := ""
- unpinSessionConn := func(connID string) {
- connID = strings.TrimSpace(connID)
- if connID == "" || pinnedSessionConnID != connID {
- return
- }
- pool.UnpinConn(account.ID, connID)
- pinnedSessionConnID = ""
- }
- pinSessionConn := func(connID string) {
- if !storeDisabled {
- return
- }
- connID = strings.TrimSpace(connID)
- if connID == "" || pinnedSessionConnID == connID {
- return
- }
- if pinnedSessionConnID != "" {
- pool.UnpinConn(account.ID, pinnedSessionConnID)
- pinnedSessionConnID = ""
- }
- if pool.PinConn(account.ID, connID) {
- pinnedSessionConnID = connID
- }
- }
- // lastTurnClean 标记最后一轮 sendAndRelay 是否正常完成(收到终端事件且客户端未断连)。
- // 所有异常路径(读写错误、error 事件、客户端断连)已在各自分支或上层(L3403)中 MarkBroken,
- // 因此 releaseSessionLease 中只需在非正常结束时 MarkBroken。
- lastTurnClean := false
- releaseSessionLease := func() {
- if sessionLease == nil {
- return
- }
- if !lastTurnClean {
- sessionLease.MarkBroken()
- }
- unpinSessionConn(sessionConnID)
- sessionLease.Release()
- if debugEnabled {
- logOpenAIWSModeDebug(
- "ingress_ws_upstream_released account_id=%d conn_id=%s",
- account.ID,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- )
- }
- }
- defer releaseSessionLease()
- turn := 1
- rejectedFieldRetryState = newOpenAIResponsesRejectedFieldRetryState(currentPayload)
- turnRetry := 0
- turnPrevRecoveryTried := false
- lastTurnFinishedAt := time.Time{}
- lastTurnResponseID := ""
- lastTurnPayload := []byte(nil)
- var lastTurnStrictState *openAIWSIngressPreviousTurnStrictState
- lastTurnReplayInput := []json.RawMessage(nil)
- lastTurnReplayInputExists := false
- currentTurnReplayInput := []json.RawMessage(nil)
- currentTurnReplayInputExists := false
- skipBeforeTurn := false
- hasCurrentOrReplayFunctionCallOutput := func(payload []byte) bool {
- if openAIWSRawPayloadHasToolCallOutput(payload) {
- return true
- }
- return currentTurnReplayInputExists && openAIWSRawItemsHasFunctionCallOutput(currentTurnReplayInput)
- }
- resetSessionLease := func(markBroken bool) {
- if sessionLease == nil {
- return
- }
- if markBroken {
- sessionLease.MarkBroken()
- }
- releaseSessionLease()
- sessionLease = nil
- sessionConnID = ""
- preferredConnID = ""
- }
- recoverIngressPrevResponseNotFound := func(relayErr error, turn int, connID string) bool {
- if !isOpenAIWSIngressPreviousResponseNotFound(relayErr) {
- return false
- }
- if turnPrevRecoveryTried || !s.openAIWSIngressPreviousResponseRecoveryEnabled() {
- return false
- }
- // 携带 function_call_output 的请求不能丢弃 previous_response_id:
- // 上游 API 需要 response chain 来匹配 tool_result 与之前的 tool_use,
- // 丢弃后会导致 "No tool call found for function call output" 400 错误。
- if hasCurrentOrReplayFunctionCallOutput(currentPayload) {
- return false
- }
- if isStrictAffinityTurn(currentPayload) {
- // Layer 2:严格亲和链路命中 previous_response_not_found 时,降级为“去掉 previous_response_id 后重放一次”。
- // 该错误说明续链锚点已失效,继续 strict fail-close 只会直接中断本轮请求。
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(storeDisabledConnMode),
- )
- }
- turnPrevRecoveryTried = true
- updatedPayload, removed, dropErr := dropPreviousResponseIDFromRawPayload(currentPayload)
- if dropErr != nil || !removed {
- reason := "not_removed"
- if dropErr != nil {
- reason = "drop_error"
- }
- logOpenAIWSModeInfo(
- "ingress_ws_prev_response_recovery_skip account_id=%d turn=%d conn_id=%s reason=%s",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(reason),
- )
- return false
- }
- updatedWithInput, setInputErr := setOpenAIWSPayloadInputSequence(
- updatedPayload,
- currentTurnReplayInput,
- currentTurnReplayInputExists,
- )
- if setInputErr != nil {
- logOpenAIWSModeInfo(
- "ingress_ws_prev_response_recovery_skip account_id=%d turn=%d conn_id=%s reason=set_full_input_error cause=%s",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(setInputErr.Error(), openAIWSLogValueMaxLen),
- )
- return false
- }
- logOpenAIWSModeInfo(
- "ingress_ws_prev_response_recovery account_id=%d turn=%d conn_id=%s action=drop_previous_response_id retry=1",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- )
- currentPayload = updatedWithInput
- currentPayloadBytes = len(updatedWithInput)
- resetSessionLease(true)
- skipBeforeTurn = true
- return true
- }
- retryIngressTurn := func(relayErr error, turn int, connID string) bool {
- if !isOpenAIWSIngressTurnRetryable(relayErr) || turnRetry >= 1 {
- return false
- }
- if isStrictAffinityTurn(currentPayload) {
- logOpenAIWSModeInfo(
- "ingress_ws_turn_retry_skip account_id=%d turn=%d conn_id=%s reason=strict_affinity",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- )
- return false
- }
- turnRetry++
- logOpenAIWSModeInfo(
- "ingress_ws_turn_retry account_id=%d turn=%d retry=%d reason=%s conn_id=%s",
- account.ID,
- turn,
- turnRetry,
- truncateOpenAIWSLogValue(openAIWSIngressTurnRetryReason(relayErr), openAIWSLogValueMaxLen),
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- )
- resetSessionLease(true)
- skipBeforeTurn = true
- return true
- }
- for {
- if turn > 1 && !skipBeforeTurn && hooks != nil && hooks.BeforeRequest != nil {
- if err := hooks.BeforeRequest(turn, currentPayload, currentOriginalModel); err != nil {
- return err
- }
- }
- if !skipBeforeTurn && hooks != nil && hooks.BeforeTurn != nil {
- if err := hooks.BeforeTurn(turn); err != nil {
- return err
- }
- }
- skipBeforeTurn = false
- // 剥离本会话已知失效的加密项,阻断同一失效密文随历史反复触发上游拒绝。
- // 历史序列须同步剥离,否则与已剥离的当前 input 项错位,prefix 复用失配。
- if invalidDigests := s.sessionInvalidEncryptedContentDigests(groupID, sessionHash); len(invalidDigests) > 0 {
- strippedPayload, strippedCount := s.stripSessionInvalidEncryptedContentLogged(
- currentPayload, invalidDigests, "ingress_ws_invalid_encrypted_lineage_strip", account.ID, turn,
- )
- if strippedCount > 0 {
- currentPayload = strippedPayload
- currentPayloadBytes = len(strippedPayload)
- }
- if lastTurnReplayInputExists {
- lastTurnReplayInput, _ = stripOpenAIInvalidEncryptedContentFromReplayItems(lastTurnReplayInput, invalidDigests)
- }
- }
- currentPreviousResponseID := openAIWSPayloadStringFromRaw(currentPayload, "previous_response_id")
- expectedPrev := strings.TrimSpace(lastTurnResponseID)
- toolSignals := ToolContinuationSignals{
- HasFunctionCallOutput: openAIWSRawPayloadHasToolCallOutput(currentPayload),
- }
- if toolSignals.HasFunctionCallOutput {
- var currentReqBody map[string]any
- if err := json.Unmarshal(currentPayload, ¤tReqBody); err == nil {
- toolSignals = AnalyzeToolContinuationSignals(currentReqBody)
- }
- }
- hasFunctionCallOutput := toolSignals.HasFunctionCallOutput
- // store=false + function_call_output 场景必须有续链锚点。
- // 若客户端未传 previous_response_id,优先回填上一轮响应 ID,避免上游报 call_id 无法关联。
- if shouldInferIngressFunctionCallOutputPreviousResponseID(
- storeDisabled,
- turn,
- toolSignals,
- currentPreviousResponseID,
- expectedPrev,
- ) {
- updatedPayload, setPrevErr := setPreviousResponseIDToRawPayload(currentPayload, expectedPrev)
- if setPrevErr != nil {
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(setPrevErr.Error(), openAIWSLogValueMaxLen),
- truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
- )
- } else {
- currentPayload = updatedPayload
- currentPayloadBytes = len(updatedPayload)
- currentPreviousResponseID = expectedPrev
- logOpenAIWSModeInfo(
- "ingress_ws_function_call_output_prev_infer account_id=%d turn=%d conn_id=%s action=set_previous_response_id previous_response_id=%s",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
- )
- }
- }
- nextReplayInput, nextReplayInputExists, replayInputErr := buildOpenAIWSReplayInputSequence(
- lastTurnReplayInput,
- lastTurnReplayInputExists,
- currentPayload,
- currentPreviousResponseID != "",
- )
- if replayInputErr != nil {
- logOpenAIWSModeInfo(
- "ingress_ws_replay_input_skip account_id=%d turn=%d conn_id=%s reason=build_error cause=%s",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(replayInputErr.Error(), openAIWSLogValueMaxLen),
- )
- currentTurnReplayInput = nil
- currentTurnReplayInputExists = false
- } else {
- currentTurnReplayInput = nextReplayInput
- currentTurnReplayInputExists = nextReplayInputExists
- }
- replayHasFunctionCallOutput := currentTurnReplayInputExists &&
- openAIWSRawItemsHasFunctionCallOutput(currentTurnReplayInput)
- hasFunctionCallOutput = hasFunctionCallOutput || replayHasFunctionCallOutput
- if storeDisabled && turn > 1 && currentPreviousResponseID != "" {
- shouldKeepPreviousResponseID := false
- strictReason := ""
- var strictErr error
- if lastTurnStrictState != nil {
- shouldKeepPreviousResponseID, strictReason, strictErr = shouldKeepIngressPreviousResponseIDWithStrictState(
- lastTurnStrictState,
- currentPayload,
- lastTurnResponseID,
- hasFunctionCallOutput,
- )
- } else {
- shouldKeepPreviousResponseID, strictReason, strictErr = shouldKeepIngressPreviousResponseID(
- lastTurnPayload,
- currentPayload,
- lastTurnResponseID,
- hasFunctionCallOutput,
- )
- }
- if strictErr != nil {
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(strictReason),
- truncateOpenAIWSLogValue(strictErr.Error(), openAIWSLogValueMaxLen),
- truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
- hasFunctionCallOutput,
- )
- } else if !shouldKeepPreviousResponseID {
- updatedPayload, removed, dropErr := dropPreviousResponseIDFromRawPayload(currentPayload)
- if dropErr != nil || !removed {
- dropReason := "not_removed"
- if dropErr != nil {
- dropReason = "drop_error"
- }
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(strictReason),
- normalizeOpenAIWSLogValue(dropReason),
- truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
- hasFunctionCallOutput,
- )
- } else {
- updatedWithInput, setInputErr := setOpenAIWSPayloadInputSequence(
- updatedPayload,
- currentTurnReplayInput,
- currentTurnReplayInputExists,
- )
- if setInputErr != nil {
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(strictReason),
- truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(setInputErr.Error(), openAIWSLogValueMaxLen),
- hasFunctionCallOutput,
- )
- } else {
- currentPayload = updatedWithInput
- currentPayloadBytes = len(updatedWithInput)
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(strictReason),
- truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
- hasFunctionCallOutput,
- )
- currentPreviousResponseID = ""
- }
- }
- }
- }
- forcePreferredConn := isStrictAffinityTurn(currentPayload)
- if sessionLease == nil {
- acquiredLease, acquireErr := acquireTurnLease(turn, preferredConnID, forcePreferredConn)
- if acquireErr != nil {
- return fmt.Errorf("acquire upstream websocket: %w", acquireErr)
- }
- sessionLease = acquiredLease
- sessionConnID = strings.TrimSpace(sessionLease.ConnID())
- if storeDisabled {
- pinSessionConn(sessionConnID)
- } else {
- unpinSessionConn(sessionConnID)
- }
- }
- shouldPreflightPing := turn > 1 && sessionLease != nil && sessionLease.SupportsIdlePingWithoutReader() && turnRetry == 0
- if shouldPreflightPing && openAIWSIngressPreflightPingIdle > 0 && !lastTurnFinishedAt.IsZero() {
- if time.Since(lastTurnFinishedAt) < openAIWSIngressPreflightPingIdle {
- shouldPreflightPing = false
- }
- }
- if shouldPreflightPing {
- if pingErr := sessionLease.PingWithTimeout(openAIWSConnHealthCheckTO); pingErr != nil {
- logOpenAIWSModeInfo(
- "ingress_ws_upstream_preflight_ping_fail account_id=%d turn=%d conn_id=%s cause=%s",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(pingErr.Error(), openAIWSLogValueMaxLen),
- )
- if forcePreferredConn {
- // 携带 function_call_output 的请求不能丢弃 previous_response_id:
- // 上游 API 需要 response chain 来匹配 tool_result 与之前的 tool_use,
- // 除非 replay input 已经包含与每个 tool_result 匹配的 tool_use 上下文。
- hasFCOutput := hasFunctionCallOutput
- hasReplayToolContext := hasFCOutput &&
- currentTurnReplayInputExists &&
- openAIWSRawItemsHaveToolCallContextForOutputs(currentTurnReplayInput)
- if !turnPrevRecoveryTried && currentPreviousResponseID != "" && (!hasFCOutput || hasReplayToolContext) {
- updatedPayload, removed, dropErr := dropPreviousResponseIDFromRawPayload(currentPayload)
- if dropErr != nil || !removed {
- reason := "not_removed"
- if dropErr != nil {
- reason = "drop_error"
- }
- logOpenAIWSModeInfo(
- "ingress_ws_preflight_ping_recovery_skip account_id=%d turn=%d conn_id=%s reason=%s previous_response_id=%s",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(reason),
- truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
- )
- } else {
- updatedWithInput, setInputErr := setOpenAIWSPayloadInputSequence(
- updatedPayload,
- currentTurnReplayInput,
- currentTurnReplayInputExists,
- )
- if setInputErr != nil {
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(setInputErr.Error(), openAIWSLogValueMaxLen),
- )
- } else {
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
- hasFCOutput,
- hasReplayToolContext,
- )
- turnPrevRecoveryTried = true
- currentPayload = updatedWithInput
- currentPayloadBytes = len(updatedWithInput)
- resetSessionLease(true)
- skipBeforeTurn = true
- continue
- }
- }
- }
- if hasFCOutput && currentPreviousResponseID != "" {
- reason := "function_call_output_missing_replay_context"
- if hasReplayToolContext {
- reason = "function_call_output_replay_not_applied"
- }
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- reason,
- truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
- hasReplayToolContext,
- )
- }
- resetSessionLease(true)
- return NewOpenAIWSClientCloseError(
- coderws.StatusPolicyViolation,
- "upstream continuation connection is unavailable; please restart the conversation",
- pingErr,
- )
- }
- resetSessionLease(true)
- acquiredLease, acquireErr := acquireTurnLease(turn, preferredConnID, forcePreferredConn)
- if acquireErr != nil {
- return fmt.Errorf("acquire upstream websocket after preflight ping fail: %w", acquireErr)
- }
- sessionLease = acquiredLease
- sessionConnID = strings.TrimSpace(sessionLease.ConnID())
- if storeDisabled {
- pinSessionConn(sessionConnID)
- }
- }
- }
- connID := sessionConnID
- if currentPreviousResponseID != "" {
- chainedFromLast := expectedPrev != "" && currentPreviousResponseID == expectedPrev
- currentPreviousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(currentPreviousResponseID)
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(currentPreviousResponseID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(currentPreviousResponseIDKind),
- truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
- chainedFromLast,
- truncateOpenAIWSLogValue(preferredConnID, openAIWSIDValueMaxLen),
- openAIWSHeaderValueForLog(baseAcquireReq.Headers, "session_id"),
- openAIWSHeaderValueForLog(baseAcquireReq.Headers, "conversation_id"),
- turnState != "",
- len(turnState),
- openAIWSPayloadStringFromRaw(currentPayload, "prompt_cache_key") != "",
- storeDisabled,
- )
- }
- result, relayErr := sendAndRelay(turn, sessionLease, currentPayload, currentPayloadBytes, currentOriginalModel, currentImageBillingModel, currentImageSizeTier, currentImageInputSize, currentRequestedReasoningEffort)
- if relayErr != nil {
- lastTurnClean = false
- if isOpenAIWSSessionPreempted(ctx) {
- sessionLease.MarkBroken()
- return errOpenAIWSSessionPreempted
- }
- var rejectedFieldErr *openAIWSRejectedFieldRetryError
- if errors.As(relayErr, &rejectedFieldErr) && rejectedFieldErr != nil && len(rejectedFieldErr.body) > 0 {
- currentPayload = append([]byte(nil), rejectedFieldErr.body...)
- currentPayloadBytes = len(currentPayload)
- skipBeforeTurn = true
- continue
- }
- if recoverIngressPrevResponseNotFound(relayErr, turn, connID) {
- continue
- }
- if retryIngressTurn(relayErr, turn, connID) {
- continue
- }
- finalErr := relayErr
- if unwrapped := errors.Unwrap(relayErr); unwrapped != nil {
- finalErr = unwrapped
- }
- if hooks != nil && hooks.AfterTurn != nil {
- hooks.AfterTurn(turn, nil, finalErr)
- }
- sessionLease.MarkBroken()
- return finalErr
- }
- turnRetry = 0
- turnPrevRecoveryTried = false
- lastTurnFinishedAt = time.Now()
- lastTurnClean = true
- if hooks != nil && hooks.AfterTurn != nil {
- hooks.AfterTurn(turn, result, nil)
- }
- if result == nil {
- return errors.New("websocket turn result is nil")
- }
- responseID := strings.TrimSpace(result.RequestID)
- lastTurnResponseID = responseID
- // 正文共享:currentPayload/currentTurnReplayInput 均不可变,历史直接引用;
- // collector 增量经 combine 合并(新头数组)。
- lastTurnReplayInput = currentTurnReplayInput
- lastTurnReplayInputExists = currentTurnReplayInputExists
- if result.wsReplayInputExists {
- lastTurnReplayInput = combineOpenAIWSReplayItems(lastTurnReplayInput, result.wsReplayInput)
- lastTurnReplayInputExists = true
- }
- nextStrictState, strictStateErr := buildOpenAIWSIngressPreviousTurnStrictState(currentPayload)
- if strictStateErr != nil {
- lastTurnStrictState = nil
- // strict 状态不可用时保留整份上一轮 payload 供慢路径比较。
- lastTurnPayload = currentPayload
- logOpenAIWSModeInfo(
- "ingress_ws_prev_response_strict_state_skip account_id=%d turn=%d conn_id=%s reason=build_error cause=%s",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(strictStateErr.Error(), openAIWSLogValueMaxLen),
- )
- } else {
- lastTurnStrictState = nextStrictState
- lastTurnPayload = nil
- }
- if responseID != "" && stateStore != nil {
- ttl := s.openAIWSResponseStickyTTL()
- logOpenAIWSBindResponseAccountWarn(groupID, account.ID, responseID, stateStore.BindResponseAccount(ctx, groupID, responseID, account.ID, ttl))
- stateStore.BindResponseConn(responseID, connID, ttl)
- }
- if stateStore != nil && storeDisabled && sessionHash != "" {
- stateStore.BindSessionConn(groupID, sessionHash, connID, s.openAIWSSessionStickyTTL())
- }
- if connID != "" {
- preferredConnID = connID
- }
- nextClientMessage, readErr := readClientMessage()
- if readErr != nil {
- if isOpenAIWSSessionPreempted(ctx) {
- return errOpenAIWSSessionPreempted
- }
- if isOpenAIWSClientDisconnectError(readErr) {
- closeStatus, closeReason := summarizeOpenAIWSReadCloseError(readErr)
- logOpenAIWSModeInfo(
- "ingress_ws_client_closed account_id=%d conn_id=%s close_status=%s close_reason=%s",
- account.ID,
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- closeStatus,
- truncateOpenAIWSLogValue(closeReason, openAIWSHeaderValueMaxLen),
- )
- return nil
- }
- return fmt.Errorf("read client websocket request: %w", readErr)
- }
- nextPayload, parseErr := parseClientPayload(turn+1, nextClientMessage)
- if parseErr != nil {
- return parseErr
- }
- nextRoutingFields := gjson.GetManyBytes(nextPayload.payloadRaw, "model", "service_tier")
- if nextPayload.promptCacheKey != "" {
- // ingress 会话在整个客户端 WS 生命周期内复用同一上游连接;
- // prompt_cache_key 对握手头的更新仅在未来需要重新建连时生效。
- updatedHeaders, _, updHdrErr := s.buildOpenAIWSHeaders(
- ctx,
- c,
- account,
- token,
- wsDecision,
- isCodexCLI,
- turnState,
- strings.TrimSpace(c.GetHeader(openAIWSTurnMetadataHeader)),
- nextPayload.promptCacheKey,
- nextRoutingFields[0].String(),
- nextRoutingFields[1].String(),
- )
- if updHdrErr != nil {
- logOpenAIWSModeInfo("ingress_ws_update_headers_failed account_id=%d err=%v", account.ID, updHdrErr)
- } else {
- baseAcquireReq.Headers = updatedHeaders
- }
- }
- setOpenAICodexRoutingHint(baseAcquireReq.Headers, account, nextRoutingFields[0].String(), nextRoutingFields[1].String())
- if nextPayload.previousResponseID != "" {
- expectedPrev := strings.TrimSpace(lastTurnResponseID)
- chainedFromLast := expectedPrev != "" && nextPayload.previousResponseID == expectedPrev
- nextPreviousResponseIDKind := ClassifyOpenAIPreviousResponseIDKind(nextPayload.previousResponseID)
- logOpenAIWSModeInfo(
- "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",
- account.ID,
- turn,
- turn+1,
- truncateOpenAIWSLogValue(connID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(nextPayload.previousResponseID, openAIWSIDValueMaxLen),
- normalizeOpenAIWSLogValue(nextPreviousResponseIDKind),
- truncateOpenAIWSLogValue(expectedPrev, openAIWSIDValueMaxLen),
- chainedFromLast,
- nextPayload.promptCacheKey != "",
- storeDisabled,
- )
- }
- if stateStore != nil && nextPayload.previousResponseID != "" {
- if stickyConnID, ok := stateStore.GetResponseConn(nextPayload.previousResponseID); ok {
- if sessionConnID != "" && stickyConnID != "" && stickyConnID != sessionConnID {
- logOpenAIWSModeInfo(
- "ingress_ws_keep_session_conn account_id=%d turn=%d conn_id=%s sticky_conn_id=%s previous_response_id=%s",
- account.ID,
- turn,
- truncateOpenAIWSLogValue(sessionConnID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(stickyConnID, openAIWSIDValueMaxLen),
- truncateOpenAIWSLogValue(nextPayload.previousResponseID, openAIWSIDValueMaxLen),
- )
- } else {
- preferredConnID = stickyConnID
- }
- }
- }
- currentPayload = nextPayload.payloadRaw
- currentOriginalModel = nextPayload.originalModel
- currentImageBillingModel = nextPayload.imageBillingModel
- currentImageSizeTier = nextPayload.imageSizeTier
- currentImageInputSize = nextPayload.imageInputSize
- currentPayloadBytes = nextPayload.payloadBytes
- currentRequestedReasoningEffort = nextPayload.requestedReasoningEffort
- rejectedFieldRetryState = newOpenAIResponsesRejectedFieldRetryState(currentPayload)
- storeDisabled = s.isOpenAIWSStoreDisabledInRequestRaw(currentPayload, account)
- if !storeDisabled {
- unpinSessionConn(sessionConnID)
- }
- turn++
- }
- }
|