Files
sub2api/backend/internal/service/openai_ws_http_bridge.go
T
Wesley Liddick 02d9901b49 Merge pull request #7038 from zenggang/contribute/gpt55-lite-compat
fix(openai): remove Responses Lite markers after OAuth mapping to GPT-5.5
2026-09-22 22:00:51 +08:00

1027 lines
36 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"slices"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
"github.com/gin-gonic/gin"
"github.com/tidwall/gjson"
)
const (
openAIWSClientReadLimitBytesDefault int64 = 64 * 1024 * 1024
openAIWSHTTPBridgeThresholdBytesDefault int64 = 15 * 1024 * 1024
openAIWSHTTPBridgeErrorBodyLimitBytes = 64 * 1024
)
const openAIWSHTTPBridgeToolStateContextKey = "openai_ws_http_bridge_tool_state"
type openAIWSHTTPBridgeToolState struct {
ClientMapping apicompat.ResponsesClientToolMapping
LoweredTools json.RawMessage
}
func openAIWSHTTPBridgeToolStateFromContext(c *gin.Context) (openAIWSHTTPBridgeToolState, bool) {
if c == nil {
return openAIWSHTTPBridgeToolState{}, false
}
value, ok := c.Get(openAIWSHTTPBridgeToolStateContextKey)
state, typed := value.(openAIWSHTTPBridgeToolState)
return state, ok && typed
}
func setOpenAIWSHTTPBridgeToolState(c *gin.Context, state openAIWSHTTPBridgeToolState) {
if c == nil {
return
}
state.LoweredTools = append(json.RawMessage(nil), state.LoweredTools...)
c.Set(openAIWSHTTPBridgeToolStateContextKey, state)
}
func decodeOpenAIWSHTTPBridgeLoweredTools(raw json.RawMessage) []any {
if len(raw) == 0 {
return nil
}
var tools []any
if err := json.Unmarshal(raw, &tools); err != nil {
return nil
}
return tools
}
func openAIWSHTTPBridgeRawField(body []byte, name string) (json.RawMessage, bool) {
var fields map[string]json.RawMessage
if err := json.Unmarshal(body, &fields); err != nil {
return nil, false
}
raw, present := fields[name]
return append(json.RawMessage(nil), raw...), present
}
func openAIWSHTTPBridgeToolUpstreamName(account *Account) string {
if account != nil && account.Platform == PlatformGrok {
return "Grok WS HTTP bridge"
}
return "OpenAI WS HTTP bridge"
}
// ResolveOpenAIWSClientFirstMessageTimeout returns the effective client ingress deadline.
func ResolveOpenAIWSClientFirstMessageTimeout(cfg *config.Config) time.Duration {
seconds := config.DefaultOpenAIWSClientFirstMessageTimeoutSeconds
if cfg != nil && cfg.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds > 0 {
seconds = cfg.Gateway.OpenAIWS.ClientFirstMessageTimeoutSeconds
}
return time.Duration(seconds) * time.Second
}
func ResolveOpenAIWSClientReadLimitBytes(cfg *config.Config) int64 {
if cfg == nil || cfg.Gateway.OpenAIWS.ClientReadLimitBytes <= 0 {
return openAIWSClientReadLimitBytesDefault
}
return cfg.Gateway.OpenAIWS.ClientReadLimitBytes
}
func (s *OpenAIGatewayService) openAIWSHTTPBridgeEnabled() bool {
return s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.HTTPBridgeEnabled
}
func (s *OpenAIGatewayService) openAIWSHTTPBridgeThresholdBytes() int64 {
if s == nil || s.cfg == nil || s.cfg.Gateway.OpenAIWS.HTTPBridgeThresholdBytes <= 0 {
return openAIWSHTTPBridgeThresholdBytesDefault
}
return s.cfg.Gateway.OpenAIWS.HTTPBridgeThresholdBytes
}
func (s *OpenAIGatewayService) shouldBridgeOpenAIWSHTTP(account *Account, payloadBytes int, previousResponseID string) bool {
if account != nil && account.Platform == PlatformGrok {
return true
}
if !s.openAIWSHTTPBridgeEnabled() {
return false
}
if strings.TrimSpace(previousResponseID) != "" {
return false
}
threshold := s.openAIWSHTTPBridgeThresholdBytes()
return threshold > 0 && int64(payloadBytes) >= threshold
}
func (s *OpenAIGatewayService) shouldBridgeOpenAIWSPassthroughFirstMessage(account *Account, payload []byte) bool {
if account != nil && account.Platform == PlatformGrok {
return true
}
if !s.openAIWSHTTPBridgeEnabled() || int64(len(payload)) < s.openAIWSHTTPBridgeThresholdBytes() {
return false
}
if !json.Valid(payload) {
return false
}
i := skipOpenAIWSJSONSpace(payload, 0)
if i >= len(payload) || payload[i] != '{' {
return false
}
i++
eventType := "response.create"
previousResponseID := ""
typeSeen, previousResponseIDSeen := false, false
for {
i = skipOpenAIWSJSONSpace(payload, i)
if payload[i] == '}' {
break
}
keyStart := i
keyEnd := scanOpenAIWSJSONString(payload, keyStart)
i = skipOpenAIWSJSONSpace(payload, keyEnd)
i++ // json.Valid guarantees the colon.
i = skipOpenAIWSJSONSpace(payload, i)
valueStart := i
i = skipOpenAIWSJSONValue(payload, i)
key := ""
// A critical key is at most 20 decoded bytes. The generous encoded bound
// covers escaped spellings without allocating attacker-sized key strings.
if keyEnd-keyStart <= 128 {
_ = json.Unmarshal(payload[keyStart:keyEnd], &key)
}
switch key {
case "type":
if typeSeen {
return false
}
typeSeen = true
var value *string
if err := json.Unmarshal(payload[valueStart:i], &value); err != nil {
return false
}
if value == nil || strings.TrimSpace(*value) == "" {
eventType = "response.create"
} else {
eventType = strings.TrimSpace(*value)
}
case "previous_response_id":
if previousResponseIDSeen {
return false
}
previousResponseIDSeen = true
var value *string
if err := json.Unmarshal(payload[valueStart:i], &value); err != nil {
return false
}
if value != nil {
previousResponseID = strings.TrimSpace(*value)
}
}
i = skipOpenAIWSJSONSpace(payload, i)
if payload[i] == ',' {
i++
}
}
return eventType == "response.create" && previousResponseID == ""
}
func skipOpenAIWSJSONSpace(payload []byte, i int) int {
for i < len(payload) {
switch payload[i] {
case ' ', '\t', '\r', '\n':
i++
default:
return i
}
}
return i
}
func scanOpenAIWSJSONString(payload []byte, i int) int {
for i++; i < len(payload); i++ {
switch payload[i] {
case '\\':
i++
case '"':
return i + 1
}
}
return len(payload)
}
func skipOpenAIWSJSONValue(payload []byte, i int) int {
if payload[i] == '"' {
return scanOpenAIWSJSONString(payload, i)
}
if payload[i] != '{' && payload[i] != '[' {
for i < len(payload) && payload[i] != ',' && payload[i] != '}' {
i++
}
return i
}
depth := 0
for ; i < len(payload); i++ {
switch payload[i] {
case '"':
i = scanOpenAIWSJSONString(payload, i) - 1
case '{', '[':
depth++
case '}', ']':
depth--
if depth == 0 {
return i + 1
}
}
}
return len(payload)
}
func prepareOpenAIWSHTTPBridgeBody(account *Account, payload []byte) ([]byte, error) {
var body map[string]any
if err := decodeOpenAIJSONUseNumber(payload, &body); err != nil {
return nil, err
}
if body == nil {
return nil, errors.New("response.create payload must be a JSON object")
}
delete(body, "type")
delete(body, "generate")
delete(body, "previous_response_id")
deleteOpenAIResponsesNoneReasoningEffortFromObject(account, body)
body["stream"] = true
return json.Marshal(body)
}
type openAIWSToolCallReplayCollector struct {
items []json.RawMessage
seen map[string]struct{}
allItems []json.RawMessage
allSeen map[string]struct{}
}
func (c *openAIWSToolCallReplayCollector) AddEvent(eventType string, message []byte) {
switch strings.TrimSpace(eventType) {
case "response.output_item.done":
item := gjson.GetBytes(message, "item")
c.addAllItem(item)
c.addItem(item)
case "response.completed", "response.done":
output := gjson.GetBytes(message, "response.output")
if !output.IsArray() {
return
}
for _, item := range output.Array() {
c.addAllItem(item)
c.addItem(item)
}
}
}
// Items/AllItems 返回浅拷贝头数组;正文由 collector 独立分配且此后不可变,
// 调用方按 replay 所有权不变式共享持有。
func (c *openAIWSToolCallReplayCollector) Items() []json.RawMessage {
return slices.Clone(c.items)
}
func (c *openAIWSToolCallReplayCollector) AllItems() []json.RawMessage {
return slices.Clone(c.allItems)
}
func (c *openAIWSToolCallReplayCollector) addAllItem(item gjson.Result) {
if !item.Exists() || item.Type != gjson.JSON {
return
}
raw := strings.TrimSpace(item.Raw)
if raw == "" || !strings.HasPrefix(raw, "{") || strings.TrimSpace(item.Get("type").String()) == "" {
return
}
key := strings.TrimSpace(item.Get("id").String())
if key == "" {
key = strings.TrimSpace(item.Get("call_id").String())
}
if key == "" {
key = raw
}
if c.allSeen == nil {
c.allSeen = make(map[string]struct{})
}
if _, ok := c.allSeen[key]; ok {
return
}
c.allSeen[key] = struct{}{}
c.allItems = append(c.allItems, json.RawMessage(raw))
}
func (c *openAIWSToolCallReplayCollector) addItem(item gjson.Result) {
if !item.Exists() || item.Type != gjson.JSON {
return
}
raw := strings.TrimSpace(item.Raw)
if raw == "" || !strings.HasPrefix(raw, "{") {
return
}
if !isCodexToolCallContextItemType(item.Get("type").String()) {
return
}
key := strings.TrimSpace(item.Get("id").String())
if key == "" {
key = strings.TrimSpace(item.Get("call_id").String())
}
if key == "" {
key = raw
}
if c.seen == nil {
c.seen = make(map[string]struct{})
}
if _, ok := c.seen[key]; ok {
return
}
c.seen[key] = struct{}{}
c.items = append(c.items, json.RawMessage(raw))
}
func buildOpenAIWSHTTPBridgeErrorEvent(statusCode int, message string) []byte {
message = strings.TrimSpace(message)
if message == "" {
message = http.StatusText(statusCode)
}
if message == "" {
message = "upstream request failed"
}
event := map[string]any{
"type": "error",
"sequence_number": 0,
"status": statusCode,
"error": map[string]any{
"type": "upstream_error",
"message": message,
},
}
body, err := json.Marshal(event)
if err != nil {
return []byte(`{"type":"error","sequence_number":0,"error":{"type":"upstream_error","message":"upstream request failed"}}`)
}
return body
}
func buildOpenAIWSHTTPBridgeFailedEvent(responseID, model string, source []byte, fallbackMessage string) []byte {
errorType := strings.TrimSpace(gjson.GetBytes(source, "error.type").String())
if errorType == "" {
errorType = strings.TrimSpace(gjson.GetBytes(source, "response.error.type").String())
}
code := strings.TrimSpace(gjson.GetBytes(source, "error.code").String())
if code == "" {
code = strings.TrimSpace(gjson.GetBytes(source, "response.error.code").String())
}
if code == "" {
code = "upstream_error"
}
message := extractOpenAISSEErrorMessage(source)
if message == "" {
message = strings.TrimSpace(fallbackMessage)
}
if message == "" {
message = "Upstream response failed"
}
errorBody := map[string]any{"code": code, "message": message}
if errorType != "" {
errorBody["type"] = errorType
}
response := map[string]any{
"id": responseID, "object": "response", "status": "failed",
"output": []any{}, "error": errorBody,
}
if model = strings.TrimSpace(model); model != "" {
response["model"] = model
}
body, err := json.Marshal(map[string]any{"type": "response.failed", "sequence_number": 0, "response": response})
if err != nil {
return []byte(`{"type":"response.failed","sequence_number":0,"response":{"status":"failed","output":[],"error":{"code":"upstream_error","message":"Upstream response failed"}}}`)
}
return body
}
func (s *OpenAIGatewayService) proxyOpenAIWSHTTPBridgeTurn(
ctx context.Context,
c *gin.Context,
account *Account,
token string,
payload []byte,
payloadBytes int,
originalModel string,
imageBillingModel string,
imageSizeTier string,
imageInputSize string,
grokCacheIdentity string,
turn int,
writeClientMessage func([]byte) error,
) (*OpenAIForwardResult, error) {
if s == nil {
return nil, errors.New("service is nil")
}
if s.httpUpstream == nil {
return nil, errors.New("openai http upstream is nil")
}
if account == nil {
return nil, errors.New("account is nil")
}
if writeClientMessage == nil {
return nil, errors.New("client websocket writer is nil")
}
responseModelObserver := &upstreamResponseModelObserver{}
body, err := prepareOpenAIWSHTTPBridgeBody(account, payload)
if err != nil {
return nil, fmt.Errorf("prepare http bridge body: %w", err)
}
grokIntentSourceBody := append([]byte(nil), body...)
_, grokExplicitToolsField := openAIWSHTTPBridgeRawField(grokIntentSourceBody, "tools")
grokExplicitToolIntent := account.Platform == PlatformGrok && hasGrokResponsesToolIntent(grokIntentSourceBody)
var clientToolMapping apicompat.ResponsesClientToolMapping
functionToolUpstream := (account.Platform == PlatformOpenAI && account.Type == AccountTypeAPIKey) || account.Platform == PlatformGrok
if functionToolUpstream {
if account.Platform == PlatformGrok {
body, err = sanitizeGrokResponsesInput(body)
if err != nil {
return nil, fmt.Errorf("sanitize Grok WS HTTP bridge input: %w", err)
}
}
inheritedState, _ := openAIWSHTTPBridgeToolStateFromContext(c)
inheritedLoweredTools := decodeOpenAIWSHTTPBridgeLoweredTools(inheritedState.LoweredTools)
body, clientToolMapping, err = adaptResponsesClientToolsForFunctionUpstreamWithMapping(
body,
openAIWSHTTPBridgeToolUpstreamName(account),
inheritedState.ClientMapping,
inheritedLoweredTools,
)
if err != nil {
return nil, fmt.Errorf("adapt %s client tools: %w", openAIWSHTTPBridgeToolUpstreamName(account), err)
}
if account.Platform == PlatformGrok && !grokExplicitToolsField && !grokExplicitToolIntent && len(inheritedLoweredTools) > 0 && hasGrokResponsesToolIntent(body) {
// This continuation omitted tools, so the pre-adapter source cannot
// represent the effective inherited declarations. Cache routing must
// see the rehydrated tool intent or it will replace client functions
// with the native-search tool-free route. Explicit current-turn tool
// intent still uses the original pre-sanitization source above.
grokIntentSourceBody = append(grokIntentSourceBody[:0], body...)
}
loweredTools := inheritedState.LoweredTools
if currentTools, present := openAIWSHTTPBridgeRawField(body, "tools"); present {
loweredTools = currentTools
}
setOpenAIWSHTTPBridgeToolState(c, openAIWSHTTPBridgeToolState{
ClientMapping: clientToolMapping,
LoweredTools: loweredTools,
})
}
if account.Platform != PlatformGrok && isOpenAIResponsesLiteWebSocketPayload(payload) {
liteBody, liteChanged, liteErr := normalizeOpenAIResponsesLitePayloadForAccount(body, account)
if liteErr != nil {
return nil, fmt.Errorf("normalize responses Lite payload: %w", liteErr)
}
if liteChanged {
body = liteBody
}
}
buildUpstreamRequest := func(requestBody []byte) (*http.Request, error) {
upstreamCtx, releaseUpstreamCtx := detachUpstreamContext(ctx)
defer releaseUpstreamCtx()
var upstreamReq *http.Request
var buildErr error
if account.Platform == PlatformGrok {
upstreamReq, buildErr = buildGrokResponsesRequest(upstreamCtx, c, account, requestBody, token, grokCacheIdentity, s.cfg, s.settingService)
} else {
upstreamReq, buildErr = s.buildUpstreamRequestOpenAIPassthrough(upstreamCtx, c, account, requestBody, token)
}
if buildErr != nil {
return nil, buildErr
}
if account.Platform != PlatformGrok && isOpenAIResponsesLiteWebSocketPayload(payload) {
upstreamReq.Header.Set(responsesLiteHeader, "true")
}
if err := applyMappedGPT55LiteCompatibility(upstreamReq, account, requestBody); err != nil {
return nil, err
}
return upstreamReq, nil
}
if account.Platform == PlatformGrok {
upstreamModel := resolveGrokWSUpstreamModel(account, body, originalModel)
body, err = patchGrokResponsesBody(body, upstreamModel)
if err != nil {
return nil, err
}
grokMixedCacheIntentBody := append([]byte(nil), body...)
body, err = applyGrokResponsesCacheIdentity(body, grokIntentSourceBody, grokCacheIdentity, account.IsGrokOAuth())
if err != nil {
return nil, fmt.Errorf("apply grok prompt cache identity: %w", err)
}
body, err = applyGrokFreeRequestToolCacheRoute(c, body, grokMixedCacheIntentBody, account, grokCacheIdentity)
if err != nil {
return nil, fmt.Errorf("apply grok Free function-tool cache route: %w", err)
}
}
actualModel := strings.TrimSpace(gjson.GetBytes(body, "model").String())
if actualModel == "" {
actualModel = canonicalOpenAIAccountSchedulingModel(account, originalModel)
}
SetOpsUpstreamModel(c, actualModel)
proxyURL := ""
if account.ProxyID != nil && account.Proxy != nil {
proxyURL = account.Proxy.URL()
}
if c != nil {
c.Set("openai_passthrough", true)
c.Set("openai_ws_http_bridge", true)
}
turnStart := time.Now()
rejectedFieldRetryState := newOpenAIResponsesRejectedFieldRetryState(body)
var resp *http.Response
for {
upstreamReq, buildErr := buildUpstreamRequest(body)
if buildErr != nil {
return nil, buildErr
}
resp, err = s.doOpenAIUpstream(upstreamReq, proxyURL, account)
if err != nil {
if turn == 1 {
return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, true)
}
safeErr := sanitizeUpstreamErrorMessage(err.Error())
clientError := buildOpenAIWSHTTPBridgeErrorEvent(http.StatusBadGateway, "Upstream request failed")
if writeErr := writeClientMessage(clientError); writeErr == nil {
markOpenAIWSClientVisibleFailure(c, "error", clientError)
}
return nil, fmt.Errorf("upstream http bridge request failed: %s", safeErr)
}
if resp.StatusCode < 400 {
break
}
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, openAIWSHTTPBridgeErrorBodyLimitBytes))
_ = resp.Body.Close()
markOpenAICyberPolicyEvent(c, respBody, resp.StatusCode, nil)
if resp.StatusCode == http.StatusBadRequest &&
extractUpstreamErrorCode(respBody) == openAIWSFallbackReasonInvalidEncryptedContent {
s.markOpenAIWSInvalidEncryptedContentLineageFromPayload(
c, body, "ingress_ws_http_bridge_invalid_encrypted_lineage_mark", account.ID, turn,
)
}
retryBody, retryReason, changed, retryErr := normalizeOpenAIResponsesRejectedFieldRetryBody(resp.StatusCode, body, respBody)
if retryErr != nil {
return nil, fmt.Errorf("normalize websocket http bridge rejected field retry: %w", retryErr)
}
if changed && rejectedFieldRetryState.Allow(retryBody) {
logOpenAIWSModeInfo(
"ingress_ws_http_bridge_rejected_field_retry account_id=%d turn=%d reason=%s",
account.ID,
turn,
truncateOpenAIWSLogValue(retryReason, openAIWSLogValueMaxLen),
)
body = retryBody
payloadBytes = len(body)
continue
}
upstreamMsg := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(respBody)))
if upstreamMsg == "" {
upstreamMsg = http.StatusText(resp.StatusCode)
}
shouldFailover := s.shouldFailoverOpenAIUpstreamResponse(account, resp.StatusCode, upstreamMsg, respBody)
if account.Platform == PlatformGrok {
shouldFailover = s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody)
s.handleGrokAccountUpstreamError(withGrokTeamRateLimitModel(ctx, resolveGrokWSUpstreamModel(account, body, originalModel)), account, resp.StatusCode, resp.Header, respBody)
if shouldFailover && (turn == 1 || resp.StatusCode == http.StatusTooManyRequests) {
return nil, newOpenAIUpstreamFailoverError(resp.StatusCode, resp.Header, respBody, upstreamMsg, false)
}
} else if shouldFailover && (turn == 1 || resp.StatusCode == http.StatusTooManyRequests) {
return nil, s.handleFailoverErrorResponsePassthrough(ctx, resp, c, account, body, respBody)
}
if account.Platform != PlatformGrok && (shouldFailover || shouldCooldownOpenAITransientUpstreamError(resp.StatusCode, respBody)) {
s.handleOpenAIAccountUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody, actualModel)
}
clientError := buildOpenAIWSHTTPBridgeErrorEvent(resp.StatusCode, upstreamMsg)
if writeErr := writeClientMessage(clientError); writeErr == nil {
markOpenAIWSClientVisibleFailure(c, "error", clientError)
}
return nil, fmt.Errorf("upstream http bridge error: status=%d message=%s", resp.StatusCode, upstreamMsg)
}
defer func() { _ = resp.Body.Close() }()
stopCancelBody := context.AfterFunc(ctx, func() { _ = resp.Body.Close() })
defer stopCancelBody()
if account.Platform == PlatformGrok {
s.updateGrokUsageFromResponse(withGrokTeamRateLimitModel(ctx, resolveGrokWSUpstreamModel(account, body, originalModel)), account, resp.Header, resp.StatusCode)
}
responseID := ""
usage := OpenAIUsage{}
imageCounter := newOpenAIImageOutputCounter()
var firstTokenMs *int
reqStream := openAIWSPayloadBoolFromRaw(body, "stream", true)
eventCount := 0
tokenEventCount := 0
terminalEventCount := 0
replayCollector := &openAIWSToolCallReplayCollector{}
firstEventType := ""
lastEventType := ""
upstreamTerminalEvent := ""
sawDone := false
wroteDownstream := false
pendingClientMessages := make([][]byte, 0, 4)
pendingClientMessageBytes := int64(0)
capacityFailoverSuppressedLogged := false
clientDisconnected := false
officialOpenAIResponses := account != nil && account.Platform == PlatformOpenAI
bareErrorPending := false
var bareErrorPayload []byte
bareErrorMessage := ""
failureAccountSideEffectsApplied := false
mappedModel := actualModel
needModelReplace := false
var mappedModelBytes []byte
if originalModel != "" {
needModelReplace = mappedModel != "" && mappedModel != originalModel
if needModelReplace {
mappedModelBytes = []byte(mappedModel)
}
}
resultWithUsage := func() *OpenAIForwardResult {
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(body)),
ReasoningEffort: ApplyThinkingEnabledFallback(extractOpenAIReasoningEffortFromBody(body, mappedModel, originalModel), body, mappedModel),
RequestedReasoningEffort: CanonicalRequestedReasoningEffort(body, originalModel, mappedModel),
Stream: reqStream,
OpenAIWSMode: true,
UpstreamTerminalEvent: upstreamTerminalEvent,
ResponseHeaders: cloneHeader(resp.Header),
Duration: time.Since(turnStart),
FirstTokenMs: firstTokenMs,
}
if replayInput := replayCollector.Items(); len(replayInput) > 0 {
result.wsReplayInput = replayInput
result.wsReplayInputExists = true
}
result.wsAccountFailoverReplayInput = replayCollector.AllItems()
if imageCount > 0 {
result.ImageCount = imageCount
result.ImageSize = imageSizeTier
result.ImageInputSize = imageInputSize
result.ImageOutputSizes = imageCounter.Sizes()
result.BillingModel = imageBillingModel
}
return result
}
maxLineSize := defaultMaxLineSize
if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
maxLineSize = s.cfg.Gateway.MaxLineSize
}
if hasResponsesClientToolMapping(clientToolMapping) {
resp.Body = newResponsesClientToolStreamBody(resp.Body, clientToolMapping, maxLineSize)
}
scanner := bufio.NewScanner(resp.Body)
scanBuf := getSSEScannerBuf64K()
scanner.Buffer(scanBuf[:0], maxLineSize)
defer putSSEScannerBuf64K(scanBuf)
pendingSSEEventType := ""
finalizeBareError := func() error {
if !bareErrorPending {
return nil
}
if !failureAccountSideEffectsApplied {
failureAccountSideEffectsApplied = s.handleOpenAIWSFailureAccountSideEffects(ctx, account, mappedModel, resp.Header, bareErrorPayload)
}
upstreamTerminalEvent = "response.failed"
if clientDisconnected {
return nil
}
clientMessage := buildOpenAIWSHTTPBridgeFailedEvent(responseID, originalModel, bareErrorPayload, bareErrorMessage)
if rewritten, changed := sanitizeOpenAICapacityShedErrorCodeForClient(clientMessage); changed {
clientMessage = rewritten
}
messages := append(pendingClientMessages, clientMessage)
pendingClientMessages = nil
pendingClientMessageBytes = 0
for _, message := range messages {
if err := writeClientMessage(message); err != nil {
if isOpenAIWSClientDisconnectError(err) {
clientDisconnected = true
return nil
}
return fmt.Errorf("write synthesized websocket response.failed: %w", err)
}
wroteDownstream = true
}
markOpenAIWSClientVisibleFailure(c, "response.failed", clientMessage)
return nil
}
for scanner.Scan() {
line := scanner.Text()
if eventType, ok := extractOpenAISSEEventLine(line); ok {
pendingSSEEventType = eventType
continue
}
if strings.TrimSpace(line) == "" {
pendingSSEEventType = ""
continue
}
data, ok := extractOpenAISSEDataLine(line)
if !ok {
continue
}
trimmedData := strings.TrimSpace(data)
if trimmedData == "" {
continue
}
if trimmedData == "[DONE]" {
sawDone = true
continue
}
upstreamMessage := []byte(openAICompatPayloadWithEventType(trimmedData, pendingSSEEventType))
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 isOpenAIWSTokenEvent(eventType) {
tokenEventCount++
if firstTokenMs == nil {
ms := int(time.Since(turnStart).Milliseconds())
firstTokenMs = &ms
}
}
if openAIWSMessageShouldParseUsage(eventType, upstreamMessage) {
parseOpenAIWSResponseUsageFromCompletedEvent(upstreamMessage, &usage)
}
if eventType == "error" || eventType == "response.failed" {
markOpenAICyberPolicyEvent(c, upstreamMessage, http.StatusOK, &usage)
}
imageCounter.AddSSEData(upstreamMessage)
if needModelReplace && len(mappedModelBytes) > 0 && openAIWSEventMayContainModel(eventType) && strings.Contains(trimmedData, mappedModel) {
upstreamMessage = replaceOpenAIWSMessageModel(upstreamMessage, mappedModel, originalModel)
}
if s.toolCorrector != nil && openAIWSEventMayContainToolCalls(eventType) && openAIWSMessageLikelyContainsToolCalls(upstreamMessage) {
if corrected, changed := s.toolCorrector.CorrectToolCallsInSSEBytes(upstreamMessage); changed {
upstreamMessage = corrected
}
}
replayCollector.AddEvent(eventType, upstreamMessage)
var upstreamEventErr error
if officialOpenAIResponses && bareErrorPending && (eventType == "response.completed" || eventType == "response.done") {
// Some upstreams emit a recoverable bare error before the authoritative
// successful terminal. Do not replace that terminal with a synthetic
// failure or retain side effects from the superseded error.
bareErrorPending = false
bareErrorPayload = nil
bareErrorMessage = ""
}
suppressClientMessage := officialOpenAIResponses && bareErrorPending && eventType != "response.failed"
if eventType == "error" || eventType == "response.failed" {
errMessage := extractOpenAISSEErrorMessage(upstreamMessage)
if errMessage == "" {
errMessage = "upstream error event"
}
statusCode := openAIStreamFailureStatus(upstreamMessage, errMessage)
shouldFailover := openAIStreamFailedEventShouldFailover(upstreamMessage, errMessage)
if eventType == "error" {
errCodeRaw, errTypeRaw, _ := parseOpenAIWSErrorEventFields(upstreamMessage)
shouldFailover = openAIStreamErrorEventShouldFailover(upstreamMessage, errMessage)
if account.Platform == PlatformGrok {
statusCode = openAIWSErrorHTTPStatusFromRaw(errCodeRaw, errTypeRaw)
}
if reason, _ := classifyOpenAIWSErrorEventFromRaw(errCodeRaw, errTypeRaw, errMessage); reason == openAIWSFallbackReasonInvalidEncryptedContent {
s.markOpenAIWSInvalidEncryptedContentLineageFromPayload(
c, body, "ingress_ws_http_bridge_invalid_encrypted_lineage_mark", account.ID, turn,
)
}
}
requestScopedCapacity := isOpenAIUpstreamCapacityShedEvent(upstreamMessage)
if account.Platform == PlatformGrok && eventType == "error" {
// SSE error events do not carry an HTTP status. The local status
// mapper therefore defaults unknown xAI codes (for example
// new_sensitive) to 502; classify the body as a request-scoped
// 403 before applying status-based failover or account state.
if isGrokContentPolicyRejection(http.StatusForbidden, upstreamMessage) {
shouldFailover = false
} else {
shouldFailover = s.shouldFailoverGrokUpstreamError(statusCode, upstreamMessage)
s.handleGrokAccountUpstreamError(ctx, account, statusCode, resp.Header, upstreamMessage)
}
}
// A disconnected client needs this attempt drained for usage, not replayed,
// even when only non-semantic heartbeats were delivered.
if !clientDisconnected && !wroteDownstream && shouldFailover && (turn == 1 || statusCode == http.StatusTooManyRequests) {
if account.Platform == PlatformGrok {
return nil, newOpenAIUpstreamFailoverError(statusCode, resp.Header, upstreamMessage, errMessage, false)
}
return nil, s.newOpenAIStreamFailoverErrorWithModel(c, account, true, resp.Header.Get("x-request-id"), upstreamMessage, errMessage, mappedModel, resp.Header)
}
if account.Platform != PlatformGrok && !failureAccountSideEffectsApplied {
if eventType == "response.failed" || (!officialOpenAIResponses && shouldFailover && !requestScopedCapacity) {
failureAccountSideEffectsApplied = s.handleOpenAIWSFailureAccountSideEffects(ctx, account, mappedModel, resp.Header, upstreamMessage)
}
}
if wroteDownstream && requestScopedCapacity && !capacityFailoverSuppressedLogged {
logOpenAICapacityFailoverSuppressed(ctx, account, "ws_http_bridge", resp.Header.Get("x-request-id"), eventType)
capacityFailoverSuppressedLogged = true
}
if eventType == "error" && !officialOpenAIResponses {
upstreamEventErr = errors.New(errMessage)
} else if eventType == "error" {
bareErrorPending = true
bareErrorPayload = append(bareErrorPayload[:0], upstreamMessage...)
bareErrorMessage = errMessage
suppressClientMessage = true
} else {
bareErrorPending = false
}
}
// 客户端写出副本改写容量降载码:Codex 对 error/response.failed 中的
// server_is_overloaded / slow_down 判致命并终止会话,改写后走客户端内置
// 重试。账号状态与终止事件判定(下方 handleOpenAIWSTerminalTransientFailure)
// 仍使用未改写的 upstreamMessage。
clientMessage := upstreamMessage
if eventType == "error" || eventType == "response.failed" {
if rewritten, changed := sanitizeOpenAICapacityShedErrorCodeForClient(clientMessage); changed {
clientMessage = rewritten
}
}
if !clientDisconnected && !suppressClientMessage {
isKeepalive := eventType == "keepalive"
stageBeforeSemanticOutput := turn == 1 && account.Platform == PlatformOpenAI && !wroteDownstream
commitStagedMessages := !stageBeforeSemanticOutput ||
openAIStreamDataStartsClientOutput(string(clientMessage), eventType) ||
isOpenAIWSTerminalEvent(eventType)
if stageBeforeSemanticOutput && !commitStagedMessages && !isKeepalive {
if pendingClientMessageBytes+int64(len(clientMessage)) > openAIFirstOutputStageMaxBytes {
return nil, s.newOpenAIStreamFailoverError(
c,
account,
true,
resp.Header.Get("x-request-id"),
nil,
"OpenAI WS HTTP bridge first-output staging limit exceeded",
resp.Header,
)
}
pendingClientMessages = append(pendingClientMessages, append([]byte(nil), clientMessage...))
pendingClientMessageBytes += int64(len(clientMessage))
} else {
// Keep the client connection alive without committing this attempt
// or exposing its staged lifecycle metadata.
var messages [][]byte
if !isKeepalive {
messages = pendingClientMessages
pendingClientMessages = nil
pendingClientMessageBytes = 0
}
messages = append(messages, clientMessage)
for _, message := range messages {
if err := writeClientMessage(message); err != nil {
if isOpenAIWSClientDisconnectError(err) {
clientDisconnected = true
closeStatus, closeReason := summarizeOpenAIWSReadCloseError(err)
logOpenAIWSModeInfo(
"ingress_ws_http_bridge_client_disconnected_drain account_id=%d turn=%d close_status=%s close_reason=%s",
account.ID,
turn,
closeStatus,
truncateOpenAIWSLogValue(closeReason, openAIWSHeaderValueMaxLen),
)
break
}
return nil, wrapOpenAIWSIngressTurnError(
"write_client",
fmt.Errorf("write client websocket event: %w", err),
wroteDownstream,
)
}
if !isKeepalive {
wroteDownstream = true
}
}
}
}
if !clientDisconnected && !suppressClientMessage {
markOpenAIWSClientVisibleFailure(c, eventType, upstreamMessage)
}
if upstreamEventErr != nil {
return resultWithUsage(), upstreamEventErr
}
if isOpenAIWSTerminalEvent(eventType) && !bareErrorPending {
if eventType == "response.failed" {
upstreamTerminalEvent = "response.failed"
} else {
upstreamTerminalEvent = s.handleOpenAIWSTerminalTransientFailure(ctx, account, mappedModel, resp.Header, upstreamMessage)
}
terminalEventCount++
firstTokenMsValue := -1
if firstTokenMs != nil {
firstTokenMsValue = *firstTokenMs
}
logOpenAIWSModeInfo(
"ingress_ws_http_bridge_turn_completed account_id=%d turn=%d response_id=%s payload_bytes=%d 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(responseID, openAIWSIDValueMaxLen),
payloadBytes,
time.Since(turnStart).Milliseconds(),
eventCount,
tokenEventCount,
terminalEventCount,
truncateOpenAIWSLogValue(firstEventType, openAIWSLogValueMaxLen),
truncateOpenAIWSLogValue(lastEventType, openAIWSLogValueMaxLen),
firstTokenMsValue,
clientDisconnected,
)
return resultWithUsage(), nil
}
}
if bareErrorPending {
if finalizeErr := finalizeBareError(); finalizeErr != nil {
return resultWithUsage(), finalizeErr
}
if scanErr := scanner.Err(); scanErr != nil {
return resultWithUsage(), fmt.Errorf("read upstream http bridge stream after error event: %w", scanErr)
}
return resultWithUsage(), errors.New(bareErrorMessage)
}
if err := scanner.Err(); err != nil {
streamErr := fmt.Errorf("read upstream http bridge stream: %w", err)
if turn == 1 && !clientDisconnected && !wroteDownstream {
return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, streamErr, true)
}
return resultWithUsage(), streamErr
}
terminalErr := errors.New("upstream http bridge stream ended before terminal event")
if sawDone {
terminalErr = errors.New("upstream http bridge stream sent [DONE] before terminal event")
}
if turn == 1 && !clientDisconnected && !wroteDownstream {
return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, terminalErr, true)
}
return resultWithUsage(), terminalErr
}
func resolveGrokWSCacheIdentity(c *gin.Context, account *Account, seedPayload, currentPayload []byte, originalModel string) (string, error) {
body, err := prepareOpenAIWSHTTPBridgeBody(account, seedPayload)
if err != nil {
return "", err
}
upstreamModel := resolveGrokWSUpstreamModel(account, currentPayload, originalModel)
body, err = patchGrokResponsesBody(body, upstreamModel)
if err != nil {
return "", err
}
return resolveGrokCacheIdentity(c, body, "", upstreamModel), nil
}
func resolveGrokWSUpstreamModel(account *Account, body []byte, originalModel string) string {
upstreamModel := strings.TrimSpace(gjson.GetBytes(body, "model").String())
originalModel = strings.TrimSpace(originalModel)
// Shared ingress has already applied channel and account mappings when the
// body model differs from the client-facing model. Only resolve from the
// original model when the body still carries that original value.
if account != nil && originalModel != "" && (upstreamModel == "" || upstreamModel == originalModel) {
if mappedModel := normalizeOpenAIModelForUpstream(account, account.GetMappedModel(originalModel)); mappedModel != "" {
upstreamModel = mappedModel
}
}
if upstreamModel == "" {
upstreamModel = grokDefaultResponsesModel
}
return upstreamModel
}