openai_ws_forwarder.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "math/rand"
  7. "strings"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  10. coderws "github.com/coder/websocket"
  11. "go.uber.org/zap"
  12. )
  13. const (
  14. openAIWSBetaV1Value = "responses_websockets=2026-02-04"
  15. openAIWSBetaV2Value = "responses_websockets=2026-02-06"
  16. openAIWSTurnStateHeader = "x-codex-turn-state"
  17. openAIWSTurnMetadataHeader = "x-codex-turn-metadata"
  18. openAIWSLogValueMaxLen = 160
  19. openAIWSHeaderValueMaxLen = 120
  20. openAIWSIDValueMaxLen = 64
  21. openAIWSEventLogHeadLimit = 20
  22. openAIWSEventLogEveryN = 50
  23. openAIWSBufferLogHeadLimit = 8
  24. openAIWSBufferLogEveryN = 20
  25. openAIWSPrewarmEventLogHead = 10
  26. openAIWSPayloadKeySizeTopN = 6
  27. openAIWSPayloadSizeEstimateDepth = 3
  28. openAIWSPayloadSizeEstimateMaxBytes = 64 * 1024
  29. openAIWSPayloadSizeEstimateMaxItems = 16
  30. openAIWSEventFlushBatchSizeDefault = 4
  31. openAIWSEventFlushIntervalDefault = 25 * time.Millisecond
  32. openAIWSPayloadLogSampleDefault = 0.2
  33. openAIWSPassthroughIdleTimeoutDefault = time.Hour
  34. openAIWSStoreDisabledConnModeStrict = "strict"
  35. openAIWSStoreDisabledConnModeAdaptive = "adaptive"
  36. openAIWSStoreDisabledConnModeOff = "off"
  37. openAIWSIngressStagePreviousResponseNotFound = "previous_response_not_found"
  38. openAIWSMaxPrevResponseIDDeletePasses = 8
  39. )
  40. var openAIWSLogValueReplacer = strings.NewReplacer(
  41. "error", "err",
  42. "fallback", "fb",
  43. "warning", "warnx",
  44. "failed", "fail",
  45. )
  46. var openAIWSIngressPreflightPingIdle = 20 * time.Second
  47. // openAIWSFallbackError 表示可安全回退到 HTTP 的 WS 错误(尚未写下游)。
  48. type openAIWSFallbackError struct {
  49. Reason string
  50. Err error
  51. }
  52. func (e *openAIWSFallbackError) Error() string {
  53. if e == nil {
  54. return ""
  55. }
  56. if e.Err == nil {
  57. return fmt.Sprintf("openai ws fallback: %s", strings.TrimSpace(e.Reason))
  58. }
  59. return fmt.Sprintf("openai ws fallback: %s: %v", strings.TrimSpace(e.Reason), e.Err)
  60. }
  61. func (e *openAIWSFallbackError) Unwrap() error {
  62. if e == nil {
  63. return nil
  64. }
  65. return e.Err
  66. }
  67. func wrapOpenAIWSFallback(reason string, err error) error {
  68. return &openAIWSFallbackError{Reason: strings.TrimSpace(reason), Err: err}
  69. }
  70. // OpenAIWSClientCloseError 表示应以指定 WebSocket close code 主动关闭客户端连接的错误。
  71. type OpenAIWSClientCloseError struct {
  72. statusCode coderws.StatusCode
  73. reason string
  74. err error
  75. }
  76. type openAIWSIngressTurnError struct {
  77. stage string
  78. cause error
  79. wroteDownstream bool
  80. }
  81. type openAIWSCurrentTurnFailoverError struct {
  82. cause error
  83. retryPayload []byte
  84. }
  85. func (e *openAIWSCurrentTurnFailoverError) Error() string {
  86. if e == nil || e.cause == nil {
  87. return "openai websocket current-turn failover"
  88. }
  89. return e.cause.Error()
  90. }
  91. func (e *openAIWSCurrentTurnFailoverError) Unwrap() error {
  92. if e == nil {
  93. return nil
  94. }
  95. return e.cause
  96. }
  97. func newOpenAIWSCurrentTurnFailoverError(cause error, retryPayload []byte) error {
  98. return &openAIWSCurrentTurnFailoverError{
  99. cause: cause,
  100. retryPayload: append([]byte(nil), retryPayload...),
  101. }
  102. }
  103. // OpenAIWSCurrentTurnRetryPayload returns an isolated copy of the payload that
  104. // may be retried on a replacement account without replaying the first turn.
  105. func OpenAIWSCurrentTurnRetryPayload(err error) ([]byte, bool) {
  106. var retryErr *openAIWSCurrentTurnFailoverError
  107. if !errors.As(err, &retryErr) || retryErr == nil {
  108. return nil, false
  109. }
  110. return append([]byte(nil), retryErr.retryPayload...), true
  111. }
  112. func (e *openAIWSIngressTurnError) Error() string {
  113. if e == nil {
  114. return ""
  115. }
  116. if e.cause == nil {
  117. return strings.TrimSpace(e.stage)
  118. }
  119. return e.cause.Error()
  120. }
  121. func (e *openAIWSIngressTurnError) Unwrap() error {
  122. if e == nil {
  123. return nil
  124. }
  125. return e.cause
  126. }
  127. func wrapOpenAIWSIngressTurnError(stage string, cause error, wroteDownstream bool) error {
  128. if cause == nil {
  129. return nil
  130. }
  131. return &openAIWSIngressTurnError{
  132. stage: strings.TrimSpace(stage),
  133. cause: cause,
  134. wroteDownstream: wroteDownstream,
  135. }
  136. }
  137. func isOpenAIWSIngressTurnRetryable(err error) bool {
  138. var turnErr *openAIWSIngressTurnError
  139. if !errors.As(err, &turnErr) || turnErr == nil {
  140. return false
  141. }
  142. if errors.Is(turnErr.cause, context.Canceled) || errors.Is(turnErr.cause, context.DeadlineExceeded) {
  143. return false
  144. }
  145. if turnErr.wroteDownstream {
  146. return false
  147. }
  148. switch turnErr.stage {
  149. case "write_upstream", "read_upstream":
  150. return true
  151. default:
  152. return false
  153. }
  154. }
  155. func openAIWSIngressTurnRetryReason(err error) string {
  156. var turnErr *openAIWSIngressTurnError
  157. if !errors.As(err, &turnErr) || turnErr == nil {
  158. return "unknown"
  159. }
  160. if turnErr.stage == "" {
  161. return "unknown"
  162. }
  163. return turnErr.stage
  164. }
  165. func isOpenAIWSIngressPreviousResponseNotFound(err error) bool {
  166. var turnErr *openAIWSIngressTurnError
  167. if !errors.As(err, &turnErr) || turnErr == nil {
  168. return false
  169. }
  170. if strings.TrimSpace(turnErr.stage) != openAIWSIngressStagePreviousResponseNotFound {
  171. return false
  172. }
  173. return !turnErr.wroteDownstream
  174. }
  175. // NewOpenAIWSClientCloseError 创建一个客户端 WS 关闭错误。
  176. func NewOpenAIWSClientCloseError(statusCode coderws.StatusCode, reason string, err error) error {
  177. return &OpenAIWSClientCloseError{
  178. statusCode: statusCode,
  179. reason: strings.TrimSpace(reason),
  180. err: err,
  181. }
  182. }
  183. func (e *OpenAIWSClientCloseError) Error() string {
  184. if e == nil {
  185. return ""
  186. }
  187. if e.err == nil {
  188. return fmt.Sprintf("openai ws client close: %d %s", int(e.statusCode), strings.TrimSpace(e.reason))
  189. }
  190. return fmt.Sprintf("openai ws client close: %d %s: %v", int(e.statusCode), strings.TrimSpace(e.reason), e.err)
  191. }
  192. func (e *OpenAIWSClientCloseError) Unwrap() error {
  193. if e == nil {
  194. return nil
  195. }
  196. return e.err
  197. }
  198. func (e *OpenAIWSClientCloseError) StatusCode() coderws.StatusCode {
  199. if e == nil {
  200. return coderws.StatusInternalError
  201. }
  202. return e.statusCode
  203. }
  204. func (e *OpenAIWSClientCloseError) Reason() string {
  205. if e == nil {
  206. return ""
  207. }
  208. return strings.TrimSpace(e.reason)
  209. }
  210. // OpenAIWSIngressHooks 定义入站 WS 每个 turn 的生命周期回调。
  211. type OpenAIWSIngressHooks struct {
  212. // ClientLifecycleContext is the request context before an ingress lease
  213. // adds its independent cancellation signal. Downstream writes bind to it
  214. // so shutdown and disconnect cancellation remain direct during lease loss.
  215. ClientLifecycleContext context.Context
  216. // InitialRequestModel is the client-facing model from the first frame,
  217. // before channel or account mapping. Ingress modes preserve it for usage
  218. // attribution while MapRequestModel determines the upstream model.
  219. InitialRequestModel string
  220. // InitialTurnStartedAt freezes when the first response.create was accepted.
  221. InitialTurnStartedAt time.Time
  222. // MaxReasoningEffort limits explicit reasoning effort values for this WS session.
  223. MaxReasoningEffort string
  224. // MaxReasoningEffortOverLimit is the access control when an explicit effort
  225. // exceeds the ceiling: downgrade (default) or deny.
  226. MaxReasoningEffortOverLimit string
  227. // ReasoningEffortMappings rewrites explicit effort values for this WS session.
  228. ReasoningEffortMappings []ReasoningEffortMapping
  229. TurnStarted func(turn int, startedAt time.Time)
  230. BeforeTurn func(turn int) error
  231. BeforeRequest func(turn int, payload []byte, originalModel string) error
  232. // MapRequestModel resolves the current turn's client model to the model
  233. // that must be written into the upstream response.create frame.
  234. MapRequestModel func(turn int, originalModel string) (string, error)
  235. AfterTurn func(turn int, result *OpenAIForwardResult, turnErr error)
  236. }
  237. func (s *OpenAIGatewayService) getOpenAIWSConnPool() *openAIWSConnPool {
  238. if s == nil {
  239. return nil
  240. }
  241. s.openaiWSPoolOnce.Do(func() {
  242. if s.openaiWSPool == nil {
  243. s.openaiWSPool = newOpenAIWSConnPool(s.cfg)
  244. }
  245. })
  246. return s.openaiWSPool
  247. }
  248. func (s *OpenAIGatewayService) getOpenAIWSPassthroughDialer() openAIWSClientDialer {
  249. if s == nil {
  250. return nil
  251. }
  252. s.openaiWSPassthroughDialerOnce.Do(func() {
  253. if s.openaiWSPassthroughDialer == nil {
  254. s.openaiWSPassthroughDialer = newDefaultOpenAIWSClientDialer()
  255. }
  256. })
  257. return s.openaiWSPassthroughDialer
  258. }
  259. func (s *OpenAIGatewayService) SnapshotOpenAIWSPoolMetrics() OpenAIWSPoolMetricsSnapshot {
  260. pool := s.getOpenAIWSConnPool()
  261. if pool == nil {
  262. return OpenAIWSPoolMetricsSnapshot{}
  263. }
  264. return pool.SnapshotMetrics()
  265. }
  266. type OpenAIWSPerformanceMetricsSnapshot struct {
  267. Pool OpenAIWSPoolMetricsSnapshot `json:"pool"`
  268. Retry OpenAIWSRetryMetricsSnapshot `json:"retry"`
  269. Transport OpenAIWSTransportMetricsSnapshot `json:"transport"`
  270. }
  271. func (s *OpenAIGatewayService) SnapshotOpenAIWSPerformanceMetrics() OpenAIWSPerformanceMetricsSnapshot {
  272. pool := s.getOpenAIWSConnPool()
  273. snapshot := OpenAIWSPerformanceMetricsSnapshot{
  274. Retry: s.SnapshotOpenAIWSRetryMetrics(),
  275. }
  276. if pool == nil {
  277. return snapshot
  278. }
  279. snapshot.Pool = pool.SnapshotMetrics()
  280. snapshot.Transport = pool.SnapshotTransportMetrics()
  281. return snapshot
  282. }
  283. func (s *OpenAIGatewayService) getOpenAIWSStateStore() OpenAIWSStateStore {
  284. if s == nil {
  285. return nil
  286. }
  287. s.openaiWSStateStoreOnce.Do(func() {
  288. if s.openaiWSStateStore == nil {
  289. s.openaiWSStateStore = NewOpenAIWSStateStore(s.cache)
  290. }
  291. })
  292. return s.openaiWSStateStore
  293. }
  294. func (s *OpenAIGatewayService) openAIWSResponseStickyTTL() time.Duration {
  295. if s != nil && s.cfg != nil {
  296. seconds := s.cfg.Gateway.OpenAIWS.StickyResponseIDTTLSeconds
  297. if seconds > 0 {
  298. return time.Duration(seconds) * time.Second
  299. }
  300. }
  301. return time.Hour
  302. }
  303. func (s *OpenAIGatewayService) openAIWSIngressPreviousResponseRecoveryEnabled() bool {
  304. if s != nil && s.cfg != nil {
  305. return s.cfg.Gateway.OpenAIWS.IngressPreviousResponseRecoveryEnabled
  306. }
  307. return true
  308. }
  309. func (s *OpenAIGatewayService) openAIWSReadTimeout() time.Duration {
  310. if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.ReadTimeoutSeconds > 0 {
  311. return time.Duration(s.cfg.Gateway.OpenAIWS.ReadTimeoutSeconds) * time.Second
  312. }
  313. return 15 * time.Minute
  314. }
  315. func (s *OpenAIGatewayService) openAIWSPassthroughIdleTimeout() time.Duration {
  316. if timeout := s.openAIWSReadTimeout(); timeout > 0 {
  317. return timeout
  318. }
  319. return openAIWSPassthroughIdleTimeoutDefault
  320. }
  321. func (s *OpenAIGatewayService) openAIWSWriteTimeout() time.Duration {
  322. if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.WriteTimeoutSeconds > 0 {
  323. return time.Duration(s.cfg.Gateway.OpenAIWS.WriteTimeoutSeconds) * time.Second
  324. }
  325. return 2 * time.Minute
  326. }
  327. func (s *OpenAIGatewayService) openAIWSEventFlushBatchSize() int {
  328. if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.EventFlushBatchSize > 0 {
  329. return s.cfg.Gateway.OpenAIWS.EventFlushBatchSize
  330. }
  331. return openAIWSEventFlushBatchSizeDefault
  332. }
  333. func (s *OpenAIGatewayService) openAIWSEventFlushInterval() time.Duration {
  334. if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.EventFlushIntervalMS >= 0 {
  335. if s.cfg.Gateway.OpenAIWS.EventFlushIntervalMS == 0 {
  336. return 0
  337. }
  338. return time.Duration(s.cfg.Gateway.OpenAIWS.EventFlushIntervalMS) * time.Millisecond
  339. }
  340. return openAIWSEventFlushIntervalDefault
  341. }
  342. func (s *OpenAIGatewayService) openAIWSPayloadLogSampleRate() float64 {
  343. if s != nil && s.cfg != nil {
  344. rate := s.cfg.Gateway.OpenAIWS.PayloadLogSampleRate
  345. if rate < 0 {
  346. return 0
  347. }
  348. if rate > 1 {
  349. return 1
  350. }
  351. return rate
  352. }
  353. return openAIWSPayloadLogSampleDefault
  354. }
  355. func (s *OpenAIGatewayService) shouldLogOpenAIWSPayloadSchema(attempt int) bool {
  356. // 首次尝试保留一条完整 payload_schema 便于排障。
  357. if attempt <= 1 {
  358. return true
  359. }
  360. rate := s.openAIWSPayloadLogSampleRate()
  361. if rate <= 0 {
  362. return false
  363. }
  364. if rate >= 1 {
  365. return true
  366. }
  367. return rand.Float64() < rate
  368. }
  369. func (s *OpenAIGatewayService) shouldEmitOpenAIWSPayloadSchema(attempt int) bool {
  370. if !s.shouldLogOpenAIWSPayloadSchema(attempt) {
  371. return false
  372. }
  373. return logger.L().Core().Enabled(zap.DebugLevel)
  374. }
  375. func (s *OpenAIGatewayService) openAIWSDialTimeout() time.Duration {
  376. if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.DialTimeoutSeconds > 0 {
  377. return time.Duration(s.cfg.Gateway.OpenAIWS.DialTimeoutSeconds) * time.Second
  378. }
  379. return 10 * time.Second
  380. }
  381. func (s *OpenAIGatewayService) openAIWSAcquireTimeout() time.Duration {
  382. // Acquire 覆盖“连接复用命中/排队/新建连接”三个阶段。
  383. // 这里不再叠加 write_timeout,避免高并发排队时把 TTFT 长尾拉到分钟级。
  384. dial := s.openAIWSDialTimeout()
  385. if dial <= 0 {
  386. dial = 10 * time.Second
  387. }
  388. return dial + 2*time.Second
  389. }