mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-12 06:40:40 +00:00
Persist reasoning ceilings and exact mappings for OpenAI groups, enforce them across HTTP and WebSocket forwarding, and invalidate cached auth snapshots.
392 lines
10 KiB
Go
392 lines
10 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
|
|
}
|
|
|
|
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 {
|
|
// InitialRequestModel 是首帧渠道映射前的请求模型,只用于 usage metadata
|
|
// 的 reasoning effort 后缀推导,禁止用于上游请求或计费模型。
|
|
InitialRequestModel string
|
|
// MaxReasoningEffort limits explicit reasoning effort values for this WS session.
|
|
MaxReasoningEffort string
|
|
// ReasoningEffortMappings rewrites explicit effort values for this WS session.
|
|
ReasoningEffortMappings []ReasoningEffortMapping
|
|
BeforeTurn func(turn int) error
|
|
BeforeRequest func(turn int, payload []byte, originalModel 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
|
|
}
|