Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
439 lines
12 KiB
Go
439 lines
12 KiB
Go
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
|
|
// 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
|
|
}
|