| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- package service
- import (
- "context"
- "errors"
- "fmt"
- "math/rand"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- coderws "github.com/coder/websocket"
- "go.uber.org/zap"
- )
- const (
- openAIWSBetaV1Value = "responses_websockets=2026-02-04"
- openAIWSBetaV2Value = "responses_websockets=2026-02-06"
- openAIWSTurnStateHeader = "x-codex-turn-state"
- openAIWSTurnMetadataHeader = "x-codex-turn-metadata"
- openAIWSLogValueMaxLen = 160
- openAIWSHeaderValueMaxLen = 120
- openAIWSIDValueMaxLen = 64
- openAIWSEventLogHeadLimit = 20
- openAIWSEventLogEveryN = 50
- openAIWSBufferLogHeadLimit = 8
- openAIWSBufferLogEveryN = 20
- openAIWSPrewarmEventLogHead = 10
- openAIWSPayloadKeySizeTopN = 6
- openAIWSPayloadSizeEstimateDepth = 3
- openAIWSPayloadSizeEstimateMaxBytes = 64 * 1024
- openAIWSPayloadSizeEstimateMaxItems = 16
- openAIWSEventFlushBatchSizeDefault = 4
- openAIWSEventFlushIntervalDefault = 25 * time.Millisecond
- openAIWSPayloadLogSampleDefault = 0.2
- openAIWSPassthroughIdleTimeoutDefault = time.Hour
- openAIWSStoreDisabledConnModeStrict = "strict"
- openAIWSStoreDisabledConnModeAdaptive = "adaptive"
- openAIWSStoreDisabledConnModeOff = "off"
- openAIWSIngressStagePreviousResponseNotFound = "previous_response_not_found"
- openAIWSMaxPrevResponseIDDeletePasses = 8
- )
- var openAIWSLogValueReplacer = strings.NewReplacer(
- "error", "err",
- "fallback", "fb",
- "warning", "warnx",
- "failed", "fail",
- )
- var openAIWSIngressPreflightPingIdle = 20 * time.Second
- // openAIWSFallbackError 表示可安全回退到 HTTP 的 WS 错误(尚未写下游)。
- type openAIWSFallbackError struct {
- Reason string
- Err error
- }
- func (e *openAIWSFallbackError) Error() string {
- if e == nil {
- return ""
- }
- if e.Err == nil {
- return fmt.Sprintf("openai ws fallback: %s", strings.TrimSpace(e.Reason))
- }
- return fmt.Sprintf("openai ws fallback: %s: %v", strings.TrimSpace(e.Reason), e.Err)
- }
- func (e *openAIWSFallbackError) Unwrap() error {
- if e == nil {
- return nil
- }
- return e.Err
- }
- func wrapOpenAIWSFallback(reason string, err error) error {
- return &openAIWSFallbackError{Reason: strings.TrimSpace(reason), Err: err}
- }
- // OpenAIWSClientCloseError 表示应以指定 WebSocket close code 主动关闭客户端连接的错误。
- type OpenAIWSClientCloseError struct {
- statusCode coderws.StatusCode
- reason string
- err error
- }
- type openAIWSIngressTurnError struct {
- stage string
- cause error
- wroteDownstream bool
- }
- type openAIWSCurrentTurnFailoverError struct {
- cause error
- retryPayload []byte
- }
- func (e *openAIWSCurrentTurnFailoverError) Error() string {
- if e == nil || e.cause == nil {
- return "openai websocket current-turn failover"
- }
- return e.cause.Error()
- }
- func (e *openAIWSCurrentTurnFailoverError) Unwrap() error {
- if e == nil {
- return nil
- }
- return e.cause
- }
- func newOpenAIWSCurrentTurnFailoverError(cause error, retryPayload []byte) error {
- return &openAIWSCurrentTurnFailoverError{
- cause: cause,
- retryPayload: append([]byte(nil), retryPayload...),
- }
- }
- // OpenAIWSCurrentTurnRetryPayload returns an isolated copy of the payload that
- // may be retried on a replacement account without replaying the first turn.
- func OpenAIWSCurrentTurnRetryPayload(err error) ([]byte, bool) {
- var retryErr *openAIWSCurrentTurnFailoverError
- if !errors.As(err, &retryErr) || retryErr == nil {
- return nil, false
- }
- return append([]byte(nil), retryErr.retryPayload...), true
- }
- func (e *openAIWSIngressTurnError) Error() string {
- if e == nil {
- return ""
- }
- if e.cause == nil {
- return strings.TrimSpace(e.stage)
- }
- return e.cause.Error()
- }
- func (e *openAIWSIngressTurnError) Unwrap() error {
- if e == nil {
- return nil
- }
- return e.cause
- }
- func wrapOpenAIWSIngressTurnError(stage string, cause error, wroteDownstream bool) error {
- if cause == nil {
- return nil
- }
- return &openAIWSIngressTurnError{
- stage: strings.TrimSpace(stage),
- cause: cause,
- wroteDownstream: wroteDownstream,
- }
- }
- func isOpenAIWSIngressTurnRetryable(err error) bool {
- var turnErr *openAIWSIngressTurnError
- if !errors.As(err, &turnErr) || turnErr == nil {
- return false
- }
- if errors.Is(turnErr.cause, context.Canceled) || errors.Is(turnErr.cause, context.DeadlineExceeded) {
- return false
- }
- if turnErr.wroteDownstream {
- return false
- }
- switch turnErr.stage {
- case "write_upstream", "read_upstream":
- return true
- default:
- return false
- }
- }
- func openAIWSIngressTurnRetryReason(err error) string {
- var turnErr *openAIWSIngressTurnError
- if !errors.As(err, &turnErr) || turnErr == nil {
- return "unknown"
- }
- if turnErr.stage == "" {
- return "unknown"
- }
- return turnErr.stage
- }
- func isOpenAIWSIngressPreviousResponseNotFound(err error) bool {
- var turnErr *openAIWSIngressTurnError
- if !errors.As(err, &turnErr) || turnErr == nil {
- return false
- }
- if strings.TrimSpace(turnErr.stage) != openAIWSIngressStagePreviousResponseNotFound {
- return false
- }
- return !turnErr.wroteDownstream
- }
- // NewOpenAIWSClientCloseError 创建一个客户端 WS 关闭错误。
- func NewOpenAIWSClientCloseError(statusCode coderws.StatusCode, reason string, err error) error {
- return &OpenAIWSClientCloseError{
- statusCode: statusCode,
- reason: strings.TrimSpace(reason),
- err: err,
- }
- }
- func (e *OpenAIWSClientCloseError) Error() string {
- if e == nil {
- return ""
- }
- if e.err == nil {
- return fmt.Sprintf("openai ws client close: %d %s", int(e.statusCode), strings.TrimSpace(e.reason))
- }
- return fmt.Sprintf("openai ws client close: %d %s: %v", int(e.statusCode), strings.TrimSpace(e.reason), e.err)
- }
- func (e *OpenAIWSClientCloseError) Unwrap() error {
- if e == nil {
- return nil
- }
- return e.err
- }
- func (e *OpenAIWSClientCloseError) StatusCode() coderws.StatusCode {
- if e == nil {
- return coderws.StatusInternalError
- }
- return e.statusCode
- }
- func (e *OpenAIWSClientCloseError) Reason() string {
- if e == nil {
- return ""
- }
- return strings.TrimSpace(e.reason)
- }
- // OpenAIWSIngressHooks 定义入站 WS 每个 turn 的生命周期回调。
- type OpenAIWSIngressHooks struct {
- // ClientLifecycleContext is the request context before an ingress lease
- // adds its independent cancellation signal. Downstream writes bind to it
- // so shutdown and disconnect cancellation remain direct during lease loss.
- ClientLifecycleContext context.Context
- // InitialRequestModel is the client-facing model from the first frame,
- // before channel or account mapping. Ingress modes preserve it for usage
- // attribution while MapRequestModel determines the upstream model.
- InitialRequestModel string
- // InitialTurnStartedAt freezes when the first response.create was accepted.
- InitialTurnStartedAt time.Time
- // MaxReasoningEffort limits explicit reasoning effort values for this WS session.
- MaxReasoningEffort string
- // MaxReasoningEffortOverLimit is the access control when an explicit effort
- // exceeds the ceiling: downgrade (default) or deny.
- MaxReasoningEffortOverLimit string
- // ReasoningEffortMappings rewrites explicit effort values for this WS session.
- ReasoningEffortMappings []ReasoningEffortMapping
- TurnStarted func(turn int, startedAt time.Time)
- BeforeTurn func(turn int) error
- BeforeRequest func(turn int, payload []byte, originalModel string) error
- // MapRequestModel resolves the current turn's client model to the model
- // that must be written into the upstream response.create frame.
- MapRequestModel func(turn int, originalModel string) (string, error)
- AfterTurn func(turn int, result *OpenAIForwardResult, turnErr error)
- }
- func (s *OpenAIGatewayService) getOpenAIWSConnPool() *openAIWSConnPool {
- if s == nil {
- return nil
- }
- s.openaiWSPoolOnce.Do(func() {
- if s.openaiWSPool == nil {
- s.openaiWSPool = newOpenAIWSConnPool(s.cfg)
- }
- })
- return s.openaiWSPool
- }
- func (s *OpenAIGatewayService) getOpenAIWSPassthroughDialer() openAIWSClientDialer {
- if s == nil {
- return nil
- }
- s.openaiWSPassthroughDialerOnce.Do(func() {
- if s.openaiWSPassthroughDialer == nil {
- s.openaiWSPassthroughDialer = newDefaultOpenAIWSClientDialer()
- }
- })
- return s.openaiWSPassthroughDialer
- }
- func (s *OpenAIGatewayService) SnapshotOpenAIWSPoolMetrics() OpenAIWSPoolMetricsSnapshot {
- pool := s.getOpenAIWSConnPool()
- if pool == nil {
- return OpenAIWSPoolMetricsSnapshot{}
- }
- return pool.SnapshotMetrics()
- }
- type OpenAIWSPerformanceMetricsSnapshot struct {
- Pool OpenAIWSPoolMetricsSnapshot `json:"pool"`
- Retry OpenAIWSRetryMetricsSnapshot `json:"retry"`
- Transport OpenAIWSTransportMetricsSnapshot `json:"transport"`
- }
- func (s *OpenAIGatewayService) SnapshotOpenAIWSPerformanceMetrics() OpenAIWSPerformanceMetricsSnapshot {
- pool := s.getOpenAIWSConnPool()
- snapshot := OpenAIWSPerformanceMetricsSnapshot{
- Retry: s.SnapshotOpenAIWSRetryMetrics(),
- }
- if pool == nil {
- return snapshot
- }
- snapshot.Pool = pool.SnapshotMetrics()
- snapshot.Transport = pool.SnapshotTransportMetrics()
- return snapshot
- }
- func (s *OpenAIGatewayService) getOpenAIWSStateStore() OpenAIWSStateStore {
- if s == nil {
- return nil
- }
- s.openaiWSStateStoreOnce.Do(func() {
- if s.openaiWSStateStore == nil {
- s.openaiWSStateStore = NewOpenAIWSStateStore(s.cache)
- }
- })
- return s.openaiWSStateStore
- }
- func (s *OpenAIGatewayService) openAIWSResponseStickyTTL() time.Duration {
- if s != nil && s.cfg != nil {
- seconds := s.cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds
- if seconds > 0 {
- return time.Duration(seconds) * time.Second
- }
- }
- return time.Hour
- }
- func (s *OpenAIGatewayService) openAIWSIngressPreviousResponseRecoveryEnabled() bool {
- if s != nil && s.cfg != nil {
- return s.cfg.Gateway.OpenAIWS.IngressPreviousResponseRecoveryEnabled
- }
- return true
- }
- func (s *OpenAIGatewayService) openAIWSReadTimeout() time.Duration {
- if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.ReadTimeoutSeconds > 0 {
- return time.Duration(s.cfg.Gateway.OpenAIWS.ReadTimeoutSeconds) * time.Second
- }
- return 15 * time.Minute
- }
- func (s *OpenAIGatewayService) openAIWSPassthroughIdleTimeout() time.Duration {
- if timeout := s.openAIWSReadTimeout(); timeout > 0 {
- return timeout
- }
- return openAIWSPassthroughIdleTimeoutDefault
- }
- func (s *OpenAIGatewayService) openAIWSWriteTimeout() time.Duration {
- if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.WriteTimeoutSeconds > 0 {
- return time.Duration(s.cfg.Gateway.OpenAIWS.WriteTimeoutSeconds) * time.Second
- }
- return 2 * time.Minute
- }
- func (s *OpenAIGatewayService) openAIWSEventFlushBatchSize() int {
- if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.EventFlushBatchSize > 0 {
- return s.cfg.Gateway.OpenAIWS.EventFlushBatchSize
- }
- return openAIWSEventFlushBatchSizeDefault
- }
- func (s *OpenAIGatewayService) openAIWSEventFlushInterval() time.Duration {
- if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.EventFlushIntervalMS >= 0 {
- if s.cfg.Gateway.OpenAIWS.EventFlushIntervalMS == 0 {
- return 0
- }
- return time.Duration(s.cfg.Gateway.OpenAIWS.EventFlushIntervalMS) * time.Millisecond
- }
- return openAIWSEventFlushIntervalDefault
- }
- func (s *OpenAIGatewayService) openAIWSPayloadLogSampleRate() float64 {
- if s != nil && s.cfg != nil {
- rate := s.cfg.Gateway.OpenAIWS.PayloadLogSampleRate
- if rate < 0 {
- return 0
- }
- if rate > 1 {
- return 1
- }
- return rate
- }
- return openAIWSPayloadLogSampleDefault
- }
- func (s *OpenAIGatewayService) shouldLogOpenAIWSPayloadSchema(attempt int) bool {
- // 首次尝试保留一条完整 payload_schema 便于排障。
- if attempt <= 1 {
- return true
- }
- rate := s.openAIWSPayloadLogSampleRate()
- if rate <= 0 {
- return false
- }
- if rate >= 1 {
- return true
- }
- return rand.Float64() < rate
- }
- func (s *OpenAIGatewayService) shouldEmitOpenAIWSPayloadSchema(attempt int) bool {
- if !s.shouldLogOpenAIWSPayloadSchema(attempt) {
- return false
- }
- return logger.L().Core().Enabled(zap.DebugLevel)
- }
- func (s *OpenAIGatewayService) openAIWSDialTimeout() time.Duration {
- if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.DialTimeoutSeconds > 0 {
- return time.Duration(s.cfg.Gateway.OpenAIWS.DialTimeoutSeconds) * time.Second
- }
- return 10 * time.Second
- }
- func (s *OpenAIGatewayService) openAIWSAcquireTimeout() time.Duration {
- // Acquire 覆盖“连接复用命中/排队/新建连接”三个阶段。
- // 这里不再叠加 write_timeout,避免高并发排队时把 TTFT 长尾拉到分钟级。
- dial := s.openAIWSDialTimeout()
- if dial <= 0 {
- dial = 10 * time.Second
- }
- return dial + 2*time.Second
- }
|