openai_live.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. package handler
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "io"
  6. "net/http"
  7. "net/url"
  8. "strconv"
  9. "strings"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
  11. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  12. "github.com/Wei-Shaw/sub2api/internal/service"
  13. coderws "github.com/coder/websocket"
  14. "github.com/gin-gonic/gin"
  15. "github.com/tidwall/gjson"
  16. "github.com/tidwall/sjson"
  17. "go.uber.org/zap"
  18. )
  19. func (h *OpenAIGatewayHandler) Live(c *gin.Context) {
  20. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  21. if !ok {
  22. h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  23. return
  24. }
  25. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  26. if !ok {
  27. h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  28. return
  29. }
  30. if apiKey.Group == nil || (apiKey.Group.Platform != service.PlatformOpenAI && apiKey.Group.Platform != service.PlatformComposite) {
  31. h.errorResponse(c, http.StatusNotFound, "not_found_error", "Live is not supported for this platform")
  32. return
  33. }
  34. if !liveEnabledForAPIKey(apiKey) {
  35. h.errorResponse(c, http.StatusForbidden, "permission_error", "Live is not enabled for this group")
  36. return
  37. }
  38. if !service.LiveBillingAvailable() {
  39. h.errorResponse(c, http.StatusServiceUnavailable, "LIVE_BILLING_UNAVAILABLE", "Live is temporarily unavailable until usage settlement is supported")
  40. return
  41. }
  42. request, err := parseLiveCallRequest(c)
  43. if err != nil {
  44. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", err.Error())
  45. return
  46. }
  47. model := strings.TrimSpace(gjson.GetBytes(request.Session, "model").String())
  48. if !compositeTargetPlatformAllowed(c, apiKey, model, service.PlatformOpenAI) {
  49. h.errorResponse(c, http.StatusNotFound, "not_found_error", "Live only supports OpenAI models for Composite groups")
  50. return
  51. }
  52. if upstreamModel, ok := service.ResolvedUpstreamModelFromContext(c.Request.Context()); ok && upstreamModel != model {
  53. rewrittenSession, rewriteErr := sjson.SetBytes(request.Session, "model", upstreamModel)
  54. if rewriteErr != nil {
  55. h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to apply Composite model route")
  56. return
  57. }
  58. request.Session = rewrittenSession
  59. }
  60. reqLog := requestLogger(
  61. c,
  62. "handler.openai_gateway.live",
  63. zap.Int64("user_id", subject.UserID),
  64. zap.Int64("api_key_id", apiKey.ID),
  65. zap.Any("group_id", apiKey.GroupID),
  66. )
  67. if decision := h.checkSecurityAudit(
  68. c,
  69. reqLog,
  70. apiKey,
  71. subject,
  72. service.ContentModerationProtocolOpenAIResponses,
  73. model,
  74. request.Session,
  75. ); decision != nil && !decision.AllowNextStage {
  76. h.openAISecurityAuditError(c, decision)
  77. return
  78. }
  79. subscription, _ := middleware2.GetSubscriptionFromContext(c)
  80. if h.billingCacheService == nil {
  81. h.errorResponse(c, http.StatusServiceUnavailable, "api_error", "Billing service unavailable")
  82. return
  83. }
  84. if err := h.billingCacheService.CheckBillingEligibility(
  85. c.Request.Context(),
  86. apiKey.User,
  87. apiKey,
  88. apiKey.Group,
  89. subscription,
  90. service.QuotaPlatform(c.Request.Context(), apiKey),
  91. ); err != nil {
  92. status, code, message, retryAfter := billingErrorDetails(err)
  93. if retryAfter > 0 {
  94. c.Header("Retry-After", strconv.Itoa(retryAfter))
  95. }
  96. h.errorResponse(c, status, code, message)
  97. return
  98. }
  99. userRelease, acquired, err := h.concurrencyHelper.TryAcquireUserSlot(
  100. c.Request.Context(),
  101. subject.UserID,
  102. subject.Concurrency,
  103. )
  104. if err != nil {
  105. h.errorResponse(c, http.StatusServiceUnavailable, "api_error", "Live concurrency unavailable")
  106. return
  107. }
  108. if !acquired {
  109. h.errorResponse(c, http.StatusTooManyRequests, "rate_limit_error", "Live concurrency limit reached")
  110. return
  111. }
  112. defer userRelease()
  113. identity := liveCallIdentity(c, apiKey, subject.UserID, subscription)
  114. created, err := h.gatewayService.CreateLiveCall(c.Request.Context(), request, identity, subject.Concurrency)
  115. if err != nil {
  116. h.writeLiveCreateError(c, err)
  117. return
  118. }
  119. c.Header("Location", liveSidebandLocation(c.FullPath(), created.CallID))
  120. c.Data(http.StatusOK, "application/sdp", created.SDP)
  121. }
  122. func parseLiveCallRequest(c *gin.Context) (*service.LiveCallRequest, error) {
  123. contentType := strings.ToLower(c.GetHeader("Content-Type"))
  124. if strings.HasPrefix(contentType, "multipart/form-data") {
  125. sdp := c.PostForm("sdp")
  126. session := json.RawMessage(c.PostForm("session"))
  127. request := &service.LiveCallRequest{SDP: sdp, Session: session}
  128. if err := service.ValidateLiveCallRequest(request); err != nil {
  129. return nil, err
  130. }
  131. return request, nil
  132. }
  133. var request service.LiveCallRequest
  134. decoder := json.NewDecoder(c.Request.Body)
  135. if err := decoder.Decode(&request); err != nil {
  136. return nil, errors.New("request body must be valid JSON")
  137. }
  138. if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
  139. return nil, errors.New("request body must contain one JSON object")
  140. }
  141. if err := service.ValidateLiveCallRequest(&request); err != nil {
  142. return nil, err
  143. }
  144. return &request, nil
  145. }
  146. func liveSidebandLocation(fullPath, callID string) string {
  147. prefix := "/v1/live/"
  148. if strings.HasPrefix(fullPath, "/backend-api/codex/") {
  149. prefix = "/backend-api/codex/"
  150. }
  151. return prefix + url.PathEscape(callID)
  152. }
  153. func liveCallIdentity(
  154. c *gin.Context,
  155. apiKey *service.APIKey,
  156. userID int64,
  157. subscription *service.UserSubscription,
  158. ) service.LiveCallIdentity {
  159. var subscriptionID *int64
  160. if subscription != nil {
  161. value := subscription.ID
  162. subscriptionID = &value
  163. }
  164. return service.LiveCallIdentity{
  165. APIKeyID: apiKey.ID,
  166. UserID: userID,
  167. GroupID: apiKey.GroupID,
  168. SubscriptionID: subscriptionID,
  169. UserAgent: c.GetHeader("User-Agent"),
  170. IPAddress: ip.GetClientIP(c),
  171. InboundEndpoint: GetInboundEndpoint(c),
  172. }
  173. }
  174. func (h *OpenAIGatewayHandler) writeLiveCreateError(c *gin.Context, err error) {
  175. switch {
  176. case errors.Is(err, service.ErrLiveConcurrencyFull):
  177. h.errorResponse(c, http.StatusTooManyRequests, "rate_limit_error", "Live concurrency limit reached")
  178. case errors.Is(err, service.ErrLiveUnavailable):
  179. h.errorResponse(c, http.StatusServiceUnavailable, "api_error", "Live is unavailable")
  180. default:
  181. var attestationErr *service.LiveAttestationUnavailableError
  182. if errors.As(err, &attestationErr) {
  183. h.errorResponse(c, http.StatusServiceUnavailable, "api_error", attestationErr.Error())
  184. return
  185. }
  186. var upstreamErr *service.UpstreamFailoverError
  187. if errors.As(err, &upstreamErr) && upstreamErr.StatusCode >= 400 && upstreamErr.StatusCode < 500 {
  188. h.errorResponse(c, upstreamErr.StatusCode, "invalid_request_error", "Live upstream rejected the request")
  189. return
  190. }
  191. h.errorResponse(c, http.StatusBadGateway, "api_error", "Live upstream request failed")
  192. }
  193. }
  194. func (h *OpenAIGatewayHandler) LiveSideband(c *gin.Context) {
  195. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  196. if !ok {
  197. h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  198. return
  199. }
  200. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  201. if !ok {
  202. h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  203. return
  204. }
  205. if !liveEnabledForAPIKey(apiKey) {
  206. h.errorResponse(c, http.StatusForbidden, "permission_error", "Live is not enabled for this group")
  207. return
  208. }
  209. identity := service.LiveCallIdentity{
  210. APIKeyID: apiKey.ID,
  211. UserID: subject.UserID,
  212. GroupID: apiKey.GroupID,
  213. }
  214. record, err := h.gatewayService.GetLiveCallForIdentity(c.Request.Context(), c.Param("call_id"), identity)
  215. if err != nil {
  216. if errors.Is(err, service.ErrLiveIdentityMismatch) {
  217. h.errorResponse(c, http.StatusForbidden, "permission_error", "Live call belongs to another identity")
  218. return
  219. }
  220. h.errorResponse(c, http.StatusNotFound, "not_found_error", "Live call not found")
  221. return
  222. }
  223. downstream, err := coderws.Accept(c.Writer, c.Request, &coderws.AcceptOptions{
  224. InsecureSkipVerify: true,
  225. })
  226. if err != nil {
  227. return
  228. }
  229. defer func() { _ = downstream.CloseNow() }()
  230. if err := h.gatewayService.ProxyLiveSideband(c.Request.Context(), record, downstream); err != nil {
  231. _ = downstream.Close(coderws.StatusInternalError, "live sideband closed")
  232. return
  233. }
  234. _ = downstream.Close(coderws.StatusNormalClosure, "")
  235. }
  236. func liveEnabledForAPIKey(apiKey *service.APIKey) bool {
  237. return apiKey != nil &&
  238. apiKey.Group != nil &&
  239. (apiKey.Group.Platform == service.PlatformOpenAI || apiKey.Group.Platform == service.PlatformComposite) &&
  240. apiKey.Group.AllowLive
  241. }