openai_live.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/sha256"
  6. "encoding/hex"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "net/url"
  13. "path"
  14. "strings"
  15. "time"
  16. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  17. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  18. coderws "github.com/coder/websocket"
  19. "github.com/google/uuid"
  20. "github.com/tidwall/gjson"
  21. "go.uber.org/zap"
  22. )
  23. var ErrLiveBillingUnavailable = infraerrors.ServiceUnavailable("LIVE_BILLING_UNAVAILABLE", "实时会话暂不可用:费用结算尚未接入")
  24. // Re-enable only together with reservation, interruption and idempotent
  25. // settlement support. A group flag must not bypass the missing billing path.
  26. func LiveBillingAvailable() bool { return false }
  27. const (
  28. defaultLiveMaxSessionDuration = time.Hour
  29. liveLeaseRefreshInterval = 20 * time.Second
  30. liveRedisOperationTimeout = 3 * time.Second
  31. liveClosedRecordTTL = 24 * time.Hour
  32. liveObserverPollInterval = 250 * time.Millisecond
  33. liveObserverStoreRetryLimit = 5
  34. liveUpstreamBodyLimit = 2 << 20
  35. )
  36. // liveObserverStoreRetryInterval 是 var 以便测试缩短 store 报错的重试等待。
  37. var liveObserverStoreRetryInterval = time.Second
  38. var (
  39. chatGPTLiveCallsURL = "https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas"
  40. chatGPTLiveSidebandBaseURL = "wss://chatgpt.com/backend-api/codex"
  41. )
  42. type liveFrameConn interface {
  43. ReadFrame(ctx context.Context) (coderws.MessageType, []byte, error)
  44. WriteFrame(ctx context.Context, msgType coderws.MessageType, payload []byte) error
  45. Close() error
  46. }
  47. func liveSidebandReadError(err error) error {
  48. if coderws.CloseStatus(err) == coderws.StatusNormalClosure {
  49. return ErrLiveCallNotFound
  50. }
  51. return err
  52. }
  53. func hashLiveCallID(callID string) string {
  54. sum := sha256.Sum256([]byte(callID))
  55. return hex.EncodeToString(sum[:])
  56. }
  57. func liveGroupID(groupID *int64) int64 {
  58. if groupID == nil {
  59. return 0
  60. }
  61. return *groupID
  62. }
  63. func liveOptionalID(value int64) *int64 {
  64. if value <= 0 {
  65. return nil
  66. }
  67. result := value
  68. return &result
  69. }
  70. func (s *OpenAIGatewayService) liveStore() (LiveCallStore, error) {
  71. if s == nil || s.cache == nil {
  72. return nil, ErrLiveUnavailable
  73. }
  74. store, ok := s.cache.(LiveCallStore)
  75. if !ok {
  76. return nil, ErrLiveUnavailable
  77. }
  78. return store, nil
  79. }
  80. func (s *OpenAIGatewayService) liveConcurrencyCache() (LiveConcurrencyCache, error) {
  81. if s == nil || s.concurrencyService == nil || s.concurrencyService.cache == nil {
  82. return nil, ErrLiveUnavailable
  83. }
  84. cache, ok := s.concurrencyService.cache.(LiveConcurrencyCache)
  85. if !ok {
  86. return nil, ErrLiveUnavailable
  87. }
  88. return cache, nil
  89. }
  90. func (s *OpenAIGatewayService) liveMaxSessionDuration() time.Duration {
  91. if s != nil && s.cfg != nil && s.cfg.Gateway.Live.MaxSessionDurationSeconds > 0 {
  92. return time.Duration(s.cfg.Gateway.Live.MaxSessionDurationSeconds) * time.Second
  93. }
  94. return defaultLiveMaxSessionDuration
  95. }
  96. func ValidateLiveCallRequest(request *LiveCallRequest) error {
  97. if request == nil || strings.TrimSpace(request.SDP) == "" {
  98. return errors.New("sdp is required")
  99. }
  100. if len(request.Session) == 0 || !json.Valid(request.Session) {
  101. return errors.New("session must be valid JSON")
  102. }
  103. var sessionObject map[string]json.RawMessage
  104. if err := json.Unmarshal(request.Session, &sessionObject); err != nil {
  105. return errors.New("session must be a JSON object")
  106. }
  107. if sessionObject == nil {
  108. return errors.New("session must be a JSON object")
  109. }
  110. return nil
  111. }
  112. // CreateLiveCall 创建 Frameless 会话。调用方须在调用期间持有普通用户槽位;
  113. // 调度器持有的普通账号槽位会被同一个 Live 租约原子接替。
  114. func (s *OpenAIGatewayService) CreateLiveCall(
  115. ctx context.Context,
  116. request *LiveCallRequest,
  117. identity LiveCallIdentity,
  118. userMaxConcurrency int,
  119. ) (*LiveCallCreated, error) {
  120. if !LiveBillingAvailable() {
  121. return nil, ErrLiveBillingUnavailable
  122. }
  123. if err := ValidateLiveCallRequest(request); err != nil {
  124. return nil, err
  125. }
  126. store, err := s.liveStore()
  127. if err != nil {
  128. return nil, err
  129. }
  130. liveCache, err := s.liveConcurrencyCache()
  131. if err != nil {
  132. return nil, err
  133. }
  134. attestation, attestationCiphertext, err := s.prepareLiveAttestation(ctx)
  135. if err != nil {
  136. return nil, err
  137. }
  138. excluded := make(map[int64]struct{})
  139. // Live 按通话时长计费,不属于 token 利润门的语义范围:显式豁免,避免
  140. // 防御性装门按文本 D 过滤 Live 账号池且门与计费时刻不同源。
  141. ctx = WithOpenAIProfitControlSuppressed(ctx)
  142. var lastErr error
  143. for attempt := 0; attempt <= 3; attempt++ {
  144. selection, _, selectErr := s.SelectAccountWithSchedulerForCapability(
  145. ctx,
  146. identity.GroupID,
  147. "",
  148. uuid.NewString(),
  149. "",
  150. excluded,
  151. OpenAIUpstreamTransportHTTPSSE,
  152. OpenAIEndpointCapabilityLive,
  153. false,
  154. false,
  155. false,
  156. )
  157. if selectErr != nil {
  158. if lastErr != nil {
  159. return nil, lastErr
  160. }
  161. return nil, selectErr
  162. }
  163. if selection == nil || selection.Account == nil || !selection.Acquired {
  164. if selection != nil && selection.ReleaseFunc != nil {
  165. selection.ReleaseFunc()
  166. }
  167. return nil, ErrLiveConcurrencyFull
  168. }
  169. account := selection.Account
  170. leaseID := generateRequestID()
  171. acquired, acquireErr := liveCache.AcquireLiveLease(
  172. ctx,
  173. account.ID,
  174. account.Concurrency,
  175. identity.UserID,
  176. userMaxConcurrency,
  177. identity.APIKeyID,
  178. leaseID,
  179. true,
  180. )
  181. if acquireErr != nil || !acquired {
  182. selection.ReleaseFunc()
  183. if acquireErr != nil {
  184. return nil, acquireErr
  185. }
  186. return nil, ErrLiveConcurrencyFull
  187. }
  188. created, createErr := s.createUpstreamLiveCall(ctx, account, request, attestation)
  189. selection.ReleaseFunc()
  190. if createErr != nil {
  191. s.releaseLiveLease(account.ID, identity.UserID, identity.APIKeyID, leaseID)
  192. if !s.shouldFailoverLiveCreateError(account, createErr) {
  193. return nil, createErr
  194. }
  195. excluded[account.ID] = struct{}{}
  196. lastErr = createErr
  197. continue
  198. }
  199. now := time.Now()
  200. model := strings.TrimSpace(gjson.GetBytes(request.Session, "model").String())
  201. if model == "" {
  202. model = "gpt-live"
  203. }
  204. record := &LiveCallRecord{
  205. CallID: created.CallID,
  206. CallHash: hashLiveCallID(created.CallID),
  207. AccountID: account.ID,
  208. APIKeyID: identity.APIKeyID,
  209. UserID: identity.UserID,
  210. GroupID: liveGroupID(identity.GroupID),
  211. SubscriptionID: liveGroupID(identity.SubscriptionID),
  212. LeaseID: leaseID,
  213. Model: model,
  214. CreatedAt: now,
  215. ExpiresAt: now.Add(s.liveMaxSessionDuration()),
  216. Controller: LiveControllerPending,
  217. UserAgent: identity.UserAgent,
  218. IPAddress: identity.IPAddress,
  219. InboundEndpoint: identity.InboundEndpoint,
  220. AttestationCiphertext: attestationCiphertext,
  221. }
  222. mappingTTL := s.liveMaxSessionDuration() + 5*time.Minute
  223. if saveErr := store.SaveLiveCall(ctx, record, mappingTTL); saveErr != nil {
  224. s.releaseLiveLease(account.ID, identity.UserID, identity.APIKeyID, leaseID)
  225. return nil, fmt.Errorf("save live call mapping: %w", saveErr)
  226. }
  227. created.Account = account
  228. go s.observeLiveCall(record)
  229. return created, nil
  230. }
  231. if lastErr != nil {
  232. return nil, lastErr
  233. }
  234. return nil, ErrLiveUnavailable
  235. }
  236. func (s *OpenAIGatewayService) shouldFailoverLiveCreateError(account *Account, err error) bool {
  237. var upstreamErr *UpstreamFailoverError
  238. if !errors.As(err, &upstreamErr) {
  239. // 凭证读取和网络传输错误都可能只影响当前账号或代理。
  240. return true
  241. }
  242. return s.shouldFailoverOpenAIUpstreamResponse(account,
  243. upstreamErr.StatusCode,
  244. "",
  245. upstreamErr.ResponseBody,
  246. )
  247. }
  248. func (s *OpenAIGatewayService) createUpstreamLiveCall(
  249. ctx context.Context,
  250. account *Account,
  251. request *LiveCallRequest,
  252. attestation string,
  253. ) (*LiveCallCreated, error) {
  254. token, _, err := s.GetAccessToken(ctx, account)
  255. if err != nil {
  256. logLiveCreateStageFailure(ctx, account.ID, "access_token", err)
  257. return nil, err
  258. }
  259. body, err := json.Marshal(struct {
  260. SDP string `json:"sdp"`
  261. Session json.RawMessage `json:"session"`
  262. }{
  263. SDP: request.SDP,
  264. Session: request.Session,
  265. })
  266. if err != nil {
  267. return nil, err
  268. }
  269. reqCtx := WithHTTPUpstreamRedirectsDisabled(WithHTTPUpstreamProfile(ctx, HTTPUpstreamProfileOpenAI))
  270. upstreamReq, err := http.NewRequestWithContext(reqCtx, http.MethodPost, chatGPTLiveCallsURL, bytes.NewReader(body))
  271. if err != nil {
  272. return nil, err
  273. }
  274. authHeaders, err := s.buildOpenAIAuthenticationHeaders(ctx, account, token)
  275. if err != nil {
  276. logLiveCreateStageFailure(ctx, account.ID, "authentication_headers", err)
  277. return nil, err
  278. }
  279. for key, values := range authHeaders {
  280. for _, value := range values {
  281. upstreamReq.Header.Add(key, value)
  282. }
  283. }
  284. upstreamReq.Host = "chatgpt.com"
  285. if err := resolveAndSetOpenAIChatGPTAccountHeaders(ctx, s.accountRepo, upstreamReq.Header, account); err != nil {
  286. logLiveCreateStageFailure(ctx, account.ID, "account_headers", err)
  287. return nil, err
  288. }
  289. upstreamReq.Header.Set("Content-Type", "application/json")
  290. upstreamReq.Header.Set("Accept", "application/sdp")
  291. upstreamReq.Header.Set(liveAttestationHeader, attestation)
  292. applyLiveUpstreamIdentityHeaders(upstreamReq.Header)
  293. resp, err := s.doOpenAIUpstream(upstreamReq, resolveAccountProxyURL(account), account)
  294. if err != nil {
  295. logLiveCreateStageFailure(ctx, account.ID, "upstream_transport", err)
  296. return nil, err
  297. }
  298. defer func() { _ = resp.Body.Close() }()
  299. responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, liveUpstreamBodyLimit+1))
  300. if readErr != nil {
  301. return nil, readErr
  302. }
  303. if len(responseBody) > liveUpstreamBodyLimit {
  304. return nil, errors.New("live upstream response is too large")
  305. }
  306. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  307. logLiveUpstreamFailure(ctx, account.ID, resp.StatusCode, resp.Header, responseBody)
  308. return nil, &UpstreamFailoverError{
  309. StatusCode: resp.StatusCode,
  310. ResponseBody: responseBody,
  311. ResponseHeaders: resp.Header.Clone(),
  312. }
  313. }
  314. callID, err := liveCallIDFromLocation(resp.Header.Get("Location"))
  315. if err != nil {
  316. return nil, err
  317. }
  318. return &LiveCallCreated{
  319. SDP: responseBody,
  320. CallID: callID,
  321. Location: resp.Header.Get("Location"),
  322. }, nil
  323. }
  324. func logLiveCreateStageFailure(ctx context.Context, accountID int64, stage string, err error) {
  325. logger.FromContext(ctx).Warn(
  326. "OpenAI Live 创建阶段失败",
  327. zap.Int64("account_id", accountID),
  328. zap.String("stage", stage),
  329. zap.String("error_type", fmt.Sprintf("%T", err)),
  330. )
  331. }
  332. func logLiveUpstreamFailure(
  333. ctx context.Context,
  334. accountID int64,
  335. statusCode int,
  336. headers http.Header,
  337. body []byte,
  338. ) {
  339. errorType := strings.TrimSpace(gjson.GetBytes(body, "error.type").String())
  340. errorCode := strings.TrimSpace(gjson.GetBytes(body, "error.code").String())
  341. errorMessage := strings.TrimSpace(gjson.GetBytes(body, "error.message").String())
  342. if errorType == "" {
  343. errorType = strings.TrimSpace(gjson.GetBytes(body, "type").String())
  344. }
  345. if errorCode == "" {
  346. errorCode = strings.TrimSpace(gjson.GetBytes(body, "code").String())
  347. }
  348. if errorMessage == "" {
  349. errorMessage = strings.TrimSpace(gjson.GetBytes(body, "message").String())
  350. }
  351. if errorMessage == "" {
  352. errorMessage = strings.TrimSpace(gjson.GetBytes(body, "detail").String())
  353. }
  354. logger.FromContext(ctx).Warn(
  355. "OpenAI Live 上游拒绝请求",
  356. zap.Int64("account_id", accountID),
  357. zap.Int("upstream_status_code", statusCode),
  358. zap.String("upstream_error_type", truncateOpenAIWSLogValue(errorType, 120)),
  359. zap.String("upstream_error_code", truncateOpenAIWSLogValue(errorCode, 120)),
  360. zap.String("upstream_error_message", truncateOpenAIWSLogValue(errorMessage, 300)),
  361. zap.String("upstream_content_type", truncateOpenAIWSLogValue(headers.Get("Content-Type"), 120)),
  362. zap.String("upstream_server", truncateOpenAIWSLogValue(headers.Get("Server"), 120)),
  363. zap.String("upstream_cf_mitigated", truncateOpenAIWSLogValue(headers.Get("Cf-Mitigated"), 120)),
  364. zap.String("upstream_cf_ray", truncateOpenAIWSLogValue(headers.Get("Cf-Ray"), 120)),
  365. zap.String("upstream_request_id", truncateOpenAIWSLogValue(headers.Get("X-Request-Id"), 120)),
  366. )
  367. }
  368. func liveCallIDFromLocation(location string) (string, error) {
  369. location = strings.TrimSpace(location)
  370. if location == "" {
  371. return "", errors.New("live upstream response has no Location")
  372. }
  373. parsed, err := url.Parse(location)
  374. if err != nil {
  375. return "", fmt.Errorf("parse live Location: %w", err)
  376. }
  377. callID := strings.TrimSpace(path.Base(strings.TrimSuffix(parsed.Path, "/")))
  378. if callID == "" || callID == "." || callID == "codex" {
  379. return "", errors.New("live upstream Location has no call id")
  380. }
  381. return callID, nil
  382. }
  383. func applyLiveUpstreamIdentityHeaders(headers http.Header) {
  384. headers.Set("OpenAI-Alpha", "quicksilver=v2")
  385. ensureCodexIdentityHeaders(headers)
  386. enforceCodexIdentityHeaders(headers)
  387. if strings.TrimSpace(headers.Get("session-id")) == "" {
  388. headers.Set("session-id", uuid.NewString())
  389. }
  390. if strings.TrimSpace(headers.Get("thread-id")) == "" {
  391. headers.Set("thread-id", uuid.NewString())
  392. }
  393. // Realtime/Live 不使用 Responses 的实验头。
  394. headers.Del("OpenAI-Beta")
  395. }
  396. func (s *OpenAIGatewayService) liveSidebandHeaders(
  397. ctx context.Context,
  398. account *Account,
  399. record *LiveCallRecord,
  400. ) (http.Header, error) {
  401. token, _, err := s.GetAccessToken(ctx, account)
  402. if err != nil {
  403. return nil, err
  404. }
  405. headers, err := s.buildOpenAIAuthenticationHeaders(ctx, account, token)
  406. if err != nil {
  407. return nil, err
  408. }
  409. if err := resolveAndSetOpenAIChatGPTAccountHeaders(ctx, s.accountRepo, headers, account); err != nil {
  410. return nil, err
  411. }
  412. attestation, err := s.decryptLiveAttestation(record)
  413. if err != nil {
  414. return nil, err
  415. }
  416. headers.Set(liveAttestationHeader, attestation)
  417. applyLiveUpstreamIdentityHeaders(headers)
  418. return headers, nil
  419. }
  420. func (s *OpenAIGatewayService) dialLiveSideband(ctx context.Context, record *LiveCallRecord) (liveFrameConn, error) {
  421. account, err := s.accountRepo.GetByID(ctx, record.AccountID)
  422. if err != nil {
  423. return nil, err
  424. }
  425. if account == nil || !account.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityLive) {
  426. return nil, ErrLiveUnavailable
  427. }
  428. headers, err := s.liveSidebandHeaders(ctx, account, record)
  429. if err != nil {
  430. return nil, err
  431. }
  432. target := strings.TrimRight(chatGPTLiveSidebandBaseURL, "/") + "/" + url.PathEscape(record.CallID)
  433. conn, status, _, err := s.getOpenAIWSPassthroughDialer().Dial(ctx, target, headers, resolveAccountProxyURL(account))
  434. if err != nil {
  435. return nil, fmt.Errorf("dial live sideband (status %d): %w", status, err)
  436. }
  437. raw, ok := conn.(liveFrameConn)
  438. if !ok {
  439. _ = conn.Close()
  440. return nil, errors.New("live sideband transport does not support raw frames")
  441. }
  442. return raw, nil
  443. }
  444. func (s *OpenAIGatewayService) GetLiveCallForIdentity(
  445. ctx context.Context,
  446. callID string,
  447. identity LiveCallIdentity,
  448. ) (*LiveCallRecord, error) {
  449. store, err := s.liveStore()
  450. if err != nil {
  451. return nil, err
  452. }
  453. record, err := store.GetLiveCall(ctx, hashLiveCallID(callID))
  454. if err != nil {
  455. return nil, err
  456. }
  457. if record.CallID != callID ||
  458. record.APIKeyID != identity.APIKeyID ||
  459. record.UserID != identity.UserID ||
  460. record.GroupID != liveGroupID(identity.GroupID) {
  461. return nil, ErrLiveIdentityMismatch
  462. }
  463. if record.Controller == LiveControllerClosed {
  464. return nil, ErrLiveCallNotFound
  465. }
  466. return record, nil
  467. }
  468. // ProxyLiveSideband 让认证后的客户端接管控制连接;媒体始终不经过这里。
  469. func (s *OpenAIGatewayService) ProxyLiveSideband(
  470. ctx context.Context,
  471. record *LiveCallRecord,
  472. downstream *coderws.Conn,
  473. ) error {
  474. if record == nil || downstream == nil {
  475. return ErrLiveCallNotFound
  476. }
  477. store, err := s.liveStore()
  478. if err != nil {
  479. return err
  480. }
  481. owner := uuid.NewString()
  482. claimed, err := store.ClaimLiveController(ctx, record.CallHash, LiveControllerProxy, owner)
  483. if err != nil {
  484. return err
  485. }
  486. if !claimed {
  487. return ErrLiveControllerChanged
  488. }
  489. // observer 轮询到接管状态后会关闭旧控制连接;同一个 call 可重新加入。
  490. time.Sleep(liveObserverPollInterval)
  491. upstream, err := s.dialLiveSideband(ctx, record)
  492. if err != nil {
  493. _, _ = store.ReleaseLiveController(context.Background(), record.CallHash, owner)
  494. go s.observeLiveCall(record)
  495. return err
  496. }
  497. defer func() { _ = upstream.Close() }()
  498. downstream.SetReadLimit(openAIWSMessageReadLimitBytes)
  499. proxyCtx, cancel := context.WithCancel(ctx)
  500. defer cancel()
  501. errCh := make(chan error, 2)
  502. go func() {
  503. for {
  504. messageType, payload, readErr := downstream.Read(proxyCtx)
  505. if readErr != nil {
  506. errCh <- readErr
  507. return
  508. }
  509. if writeErr := upstream.WriteFrame(proxyCtx, messageType, payload); writeErr != nil {
  510. errCh <- writeErr
  511. return
  512. }
  513. }
  514. }()
  515. go func() {
  516. for {
  517. messageType, payload, readErr := upstream.ReadFrame(proxyCtx)
  518. if readErr != nil {
  519. errCh <- liveSidebandReadError(readErr)
  520. return
  521. }
  522. if writeErr := downstream.Write(proxyCtx, messageType, payload); writeErr != nil {
  523. errCh <- writeErr
  524. return
  525. }
  526. if messageType == coderws.MessageText {
  527. eventType := strings.TrimSpace(gjson.GetBytes(payload, "type").String())
  528. if eventType == "session.closed" || eventType == "session.ended" {
  529. errCh <- ErrLiveCallNotFound
  530. return
  531. }
  532. }
  533. }
  534. }()
  535. runErr := s.runLiveController(proxyCtx, record, upstream, errCh)
  536. cancel()
  537. _, _ = store.ReleaseLiveController(context.Background(), record.CallHash, owner)
  538. if liveSessionEnded(runErr) || !time.Now().Before(record.ExpiresAt) {
  539. s.finalizeLiveCall(record)
  540. return runErr
  541. }
  542. go s.observeLiveCall(record)
  543. return runErr
  544. }
  545. // liveSessionEnded 判断控制连接的退出原因是否意味着会话已终结(应 finalize:写
  546. // usage log 并释放租约),而不是可以交给 observer 重连的临时错误。
  547. //
  548. // ErrLiveUnavailable 在控制循环里只会来自租约续租失败。RefreshLiveLease 的 Lua 在
  549. // leaseID 被 GC 后不会重新写入,重连也拿不回并发槽 —— 若按临时错误重试,会话会以
  550. // 约 1 秒一轮的节奏空转到 ExpiresAt,期间持着上游连接却不计入任何并发限制。
  551. func liveSessionEnded(err error) bool {
  552. return errors.Is(err, ErrLiveCallNotFound) ||
  553. errors.Is(err, ErrLiveUnavailable) ||
  554. errors.Is(err, context.DeadlineExceeded)
  555. }
  556. func (s *OpenAIGatewayService) runLiveController(
  557. ctx context.Context,
  558. record *LiveCallRecord,
  559. upstream liveFrameConn,
  560. errCh <-chan error,
  561. ) error {
  562. refreshTicker := time.NewTicker(liveLeaseRefreshInterval)
  563. defer refreshTicker.Stop()
  564. maxTimer := time.NewTimer(time.Until(record.ExpiresAt))
  565. defer maxTimer.Stop()
  566. for {
  567. select {
  568. case <-ctx.Done():
  569. return context.Cause(ctx)
  570. case err := <-errCh:
  571. return err
  572. case <-maxTimer.C:
  573. closeCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
  574. _ = upstream.WriteFrame(closeCtx, coderws.MessageText, []byte(`{"type":"session.close"}`))
  575. cancel()
  576. return context.DeadlineExceeded
  577. case <-refreshTicker.C:
  578. if !s.refreshLiveLease(record) {
  579. return ErrLiveUnavailable
  580. }
  581. }
  582. }
  583. }
  584. func (s *OpenAIGatewayService) observeLiveCall(record *LiveCallRecord) {
  585. if record == nil {
  586. return
  587. }
  588. store, err := s.liveStore()
  589. if err != nil {
  590. return
  591. }
  592. owner := uuid.NewString()
  593. claimed, claimErr := store.ClaimLiveController(context.Background(), record.CallHash, LiveControllerObserver, owner)
  594. if claimErr != nil {
  595. // store 报错时无法确认控制权归属,不能静默退出:若 claim 实际已生效而
  596. // observer 消失,租约与 usage log 都会丢。兜底 finalize 是幂等的,即使
  597. // 控制权在他人手上也只会在到期后落一次库。
  598. s.finalizeLiveCallAfterExpiry(record)
  599. return
  600. }
  601. if !claimed {
  602. return
  603. }
  604. storeErrStreak := 0
  605. for {
  606. latest, getErr := store.GetLiveCall(context.Background(), record.CallHash)
  607. if getErr != nil {
  608. // 记录已被清理(closed TTL 到期)不是故障,直接退出。
  609. if errors.Is(getErr, ErrLiveCallNotFound) {
  610. return
  611. }
  612. // store 抖动不等于控制权被接管:有限次重试;仍失败则按
  613. // record.ExpiresAt 兜底 finalize,保证 usage log 与租约释放不丢。
  614. storeErrStreak++
  615. if storeErrStreak >= liveObserverStoreRetryLimit {
  616. s.finalizeLiveCallAfterExpiry(record)
  617. return
  618. }
  619. time.Sleep(liveObserverStoreRetryInterval)
  620. continue
  621. }
  622. storeErrStreak = 0
  623. record = latest
  624. if record.Controller != LiveControllerObserver {
  625. return
  626. }
  627. if !time.Now().Before(record.ExpiresAt) {
  628. s.finalizeLiveCall(record)
  629. return
  630. }
  631. upstream, dialErr := s.dialLiveSideband(context.Background(), record)
  632. if dialErr != nil {
  633. if !s.waitForLiveObserverRetry(record) {
  634. return
  635. }
  636. continue
  637. }
  638. runErr := s.runLiveObserverConnection(record, upstream)
  639. _ = upstream.Close()
  640. if errors.Is(runErr, ErrLiveControllerChanged) {
  641. return
  642. }
  643. if liveSessionEnded(runErr) {
  644. s.finalizeLiveCall(record)
  645. return
  646. }
  647. if !s.waitForLiveObserverRetry(record) {
  648. return
  649. }
  650. }
  651. }
  652. func (s *OpenAIGatewayService) runLiveObserverConnection(record *LiveCallRecord, upstream liveFrameConn) error {
  653. ctx, cancel := context.WithCancel(context.Background())
  654. defer cancel()
  655. frameCh := make(chan []byte, 1)
  656. errCh := make(chan error, 1)
  657. go func() {
  658. for {
  659. messageType, payload, err := upstream.ReadFrame(ctx)
  660. if err != nil {
  661. select {
  662. case errCh <- liveSidebandReadError(err):
  663. case <-ctx.Done():
  664. }
  665. return
  666. }
  667. if messageType == coderws.MessageText {
  668. select {
  669. case frameCh <- payload:
  670. case <-ctx.Done():
  671. return
  672. }
  673. }
  674. }
  675. }()
  676. refreshTicker := time.NewTicker(liveLeaseRefreshInterval)
  677. defer refreshTicker.Stop()
  678. controllerTicker := time.NewTicker(liveObserverPollInterval)
  679. defer controllerTicker.Stop()
  680. maxTimer := time.NewTimer(time.Until(record.ExpiresAt))
  681. defer maxTimer.Stop()
  682. store, _ := s.liveStore()
  683. for {
  684. select {
  685. case payload := <-frameCh:
  686. eventType := strings.TrimSpace(gjson.GetBytes(payload, "type").String())
  687. if eventType == "session.closed" || eventType == "session.ended" {
  688. return ErrLiveCallNotFound
  689. }
  690. case err := <-errCh:
  691. return err
  692. case <-controllerTicker.C:
  693. controller, err := store.GetLiveController(context.Background(), record.CallHash)
  694. if err != nil {
  695. return err
  696. }
  697. if controller != LiveControllerObserver {
  698. return ErrLiveControllerChanged
  699. }
  700. case <-refreshTicker.C:
  701. if !s.refreshLiveLease(record) {
  702. return ErrLiveUnavailable
  703. }
  704. case <-maxTimer.C:
  705. closeCtx, closeCancel := context.WithTimeout(context.Background(), 2*time.Second)
  706. _ = upstream.WriteFrame(closeCtx, coderws.MessageText, []byte(`{"type":"session.close"}`))
  707. closeCancel()
  708. return context.DeadlineExceeded
  709. }
  710. }
  711. }
  712. func (s *OpenAIGatewayService) waitForLiveObserverRetry(record *LiveCallRecord) bool {
  713. timer := time.NewTimer(time.Second)
  714. defer timer.Stop()
  715. <-timer.C
  716. store, err := s.liveStore()
  717. if err != nil {
  718. return false
  719. }
  720. controller, getErr := store.GetLiveController(context.Background(), record.CallHash)
  721. if getErr != nil && !errors.Is(getErr, ErrLiveCallNotFound) {
  722. // store 报错不等于控制权被接管:返回 true 交回 observeLiveCall 循环顶部,
  723. // 由它对 store 故障做有限次重试与 ExpiresAt 兜底 finalize。在这里返回
  724. // false 会让 Redis 抖动时会话静默结束、不留记录。
  725. return true
  726. }
  727. // 过期不在此处判定:返回 true 让调用方回到循环顶部的过期分支,由它 finalize
  728. // (写 usage log + 释放租约)。在这里直接返回 false 会让会话静默结束、不留记录。
  729. return getErr == nil && controller == LiveControllerObserver
  730. }
  731. // finalizeLiveCallAfterExpiry 是 store 持续报错、observer 无法继续观察时的兜底:
  732. // 等到会话最长时限 ExpiresAt 再 finalize,保证 usage log 与租约释放最迟在会话到期
  733. // 时完成。MarkLiveCallClosed 的 first 语义保证与其他恢复路径不会重复落库。
  734. func (s *OpenAIGatewayService) finalizeLiveCallAfterExpiry(record *LiveCallRecord) {
  735. if record == nil {
  736. return
  737. }
  738. if wait := time.Until(record.ExpiresAt); wait > 0 {
  739. time.Sleep(wait)
  740. }
  741. s.finalizeLiveCall(record)
  742. }
  743. func (s *OpenAIGatewayService) refreshLiveLease(record *LiveCallRecord) bool {
  744. cache, err := s.liveConcurrencyCache()
  745. if err != nil {
  746. return false
  747. }
  748. ctx, cancel := context.WithTimeout(context.Background(), liveRedisOperationTimeout)
  749. defer cancel()
  750. refreshed, err := cache.RefreshLiveLease(ctx, record.AccountID, record.UserID, record.APIKeyID, record.LeaseID)
  751. return err == nil && refreshed
  752. }
  753. func (s *OpenAIGatewayService) releaseLiveLease(accountID, userID, apiKeyID int64, leaseID string) {
  754. cache, err := s.liveConcurrencyCache()
  755. if err != nil {
  756. return
  757. }
  758. ctx, cancel := context.WithTimeout(context.Background(), liveRedisOperationTimeout)
  759. defer cancel()
  760. _ = cache.ReleaseLiveLease(ctx, accountID, userID, apiKeyID, leaseID)
  761. }
  762. func (s *OpenAIGatewayService) finalizeLiveCall(record *LiveCallRecord) {
  763. if record == nil {
  764. return
  765. }
  766. store, err := s.liveStore()
  767. if err != nil {
  768. return
  769. }
  770. ctx, cancel := context.WithTimeout(context.Background(), liveRedisOperationTimeout)
  771. first, err := store.MarkLiveCallClosed(ctx, record.CallHash, liveClosedRecordTTL)
  772. cancel()
  773. if err != nil || !first {
  774. return
  775. }
  776. s.releaseLiveLease(record.AccountID, record.UserID, record.APIKeyID, record.LeaseID)
  777. if s.usageLogRepo == nil {
  778. return
  779. }
  780. duration := int(time.Since(record.CreatedAt).Milliseconds())
  781. if duration < 0 {
  782. duration = 0
  783. }
  784. inboundEndpoint := record.InboundEndpoint
  785. upstreamEndpoint := "/backend-api/codex/realtime/calls"
  786. userAgent := record.UserAgent
  787. ipAddress := record.IPAddress
  788. billingType := int8(BillingTypeBalance)
  789. if record.SubscriptionID > 0 {
  790. billingType = BillingTypeSubscription
  791. }
  792. // TODO(billing): Live 会话目前不计费:TotalCost/ActualCost 恒为 0,完全绕过
  793. // recordUsageCore/applyUsageBilling,余额模式下极低余额也能反复开启最长
  794. // liveMaxSessionDuration 的会话。若确认按时长计费,应在此接入计费管道;
  795. // 若确认有意免费,删除本注释即可(零值行为由
  796. // TestFinalizeLiveCallIsIdempotentAndWritesZeroUsage 锁定)。
  797. //
  798. // 这是该会话唯一一次落库机会(MarkLiveCallClosed 已标记 first),失败即永久
  799. // 丢失,因此走带日志与同步兜底的 writeUsageLogBestEffort(issue #3656)。
  800. writeUsageLogBestEffort(context.Background(), s.usageLogRepo, &UsageLog{
  801. UserID: record.UserID,
  802. APIKeyID: record.APIKeyID,
  803. AccountID: record.AccountID,
  804. RequestID: record.CallHash,
  805. Model: record.Model,
  806. RequestedModel: record.Model,
  807. GroupID: liveOptionalID(record.GroupID),
  808. SubscriptionID: liveOptionalID(record.SubscriptionID),
  809. RateMultiplier: 1,
  810. BillingType: billingType,
  811. RequestType: RequestTypeLive,
  812. DurationMs: &duration,
  813. UserAgent: &userAgent,
  814. IPAddress: &ipAddress,
  815. InboundEndpoint: &inboundEndpoint,
  816. UpstreamEndpoint: &upstreamEndpoint,
  817. CreatedAt: record.CreatedAt,
  818. }, "service.openai_live")
  819. }