openai_images.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. package handler
  2. import (
  3. "context"
  4. "errors"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. pkghttputil "github.com/Wei-Shaw/sub2api/internal/pkg/httputil"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  12. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  13. "github.com/Wei-Shaw/sub2api/internal/service"
  14. "github.com/gin-gonic/gin"
  15. "go.uber.org/zap"
  16. )
  17. // Images handles OpenAI Images API requests.
  18. // POST /v1/images/generations
  19. // POST /v1/images/edits
  20. func (h *OpenAIGatewayHandler) Images(c *gin.Context) {
  21. streamStarted := false
  22. defer h.recoverResponsesPanic(c, &streamStarted)
  23. requestStart := time.Now()
  24. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  25. if !ok {
  26. h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
  27. return
  28. }
  29. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  30. if !ok {
  31. h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
  32. return
  33. }
  34. reqLog := requestLogger(
  35. c,
  36. "handler.openai_gateway.images",
  37. zap.Int64("user_id", subject.UserID),
  38. zap.Int64("api_key_id", apiKey.ID),
  39. zap.Any("group_id", apiKey.GroupID),
  40. )
  41. if !h.ensureResponsesDependencies(c, reqLog) {
  42. return
  43. }
  44. body, err := pkghttputil.ReadRequestBodyWithPrealloc(c.Request)
  45. if err != nil {
  46. if maxErr, ok := extractMaxBytesError(err); ok {
  47. h.errorResponse(c, http.StatusRequestEntityTooLarge, "invalid_request_error", buildBodyTooLargeMessage(maxErr.Limit))
  48. return
  49. }
  50. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body")
  51. return
  52. }
  53. if len(body) == 0 {
  54. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
  55. return
  56. }
  57. if isMultipartImagesContentType(c.GetHeader("Content-Type")) {
  58. setOpsRequestContext(c, "", false)
  59. } else {
  60. setOpsRequestContext(c, "", false)
  61. }
  62. parsed, err := h.gatewayService.ParseOpenAIImagesRequest(c, body)
  63. if err != nil {
  64. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", err.Error())
  65. return
  66. }
  67. requestModel := parsed.Model
  68. ensureCompositeTargetPlatform(c, apiKey, requestModel)
  69. clientRequestModel := clientRequestedModel(c, requestModel)
  70. routingModel := requestModel
  71. if resolvedModel, ok := service.ResolvedUpstreamModelFromContext(c.Request.Context()); ok {
  72. routingModel = resolvedModel
  73. }
  74. if !compositeTargetPlatformAllowed(c, apiKey, requestModel, service.PlatformOpenAI) {
  75. h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
  76. return
  77. }
  78. reqLog = reqLog.With(
  79. zap.String("model", clientRequestModel),
  80. zap.String("routing_model", routingModel),
  81. zap.Bool("stream", parsed.Stream),
  82. zap.Bool("multipart", parsed.Multipart),
  83. zap.String("capability", string(parsed.RequiredCapability)),
  84. zap.String("img_quality", parsed.Quality),
  85. zap.String("img_size", parsed.Size),
  86. )
  87. if !service.GroupAllowsImageGeneration(apiKey.Group) {
  88. h.errorResponse(c, http.StatusForbidden, "permission_error", service.ImageGenerationPermissionMessage())
  89. return
  90. }
  91. if decision := h.checkSecurityAudit(c, reqLog, apiKey, subject, service.ContentModerationProtocolOpenAIImages, requestModel, parsed.ModerationBody()); decision != nil && !decision.AllowNextStage {
  92. h.openAISecurityAuditError(c, decision)
  93. return
  94. }
  95. imageReleaseFunc, acquired := h.acquireImageGenerationSlot(c, streamStarted)
  96. if !acquired {
  97. return
  98. }
  99. if imageReleaseFunc != nil {
  100. defer imageReleaseFunc()
  101. }
  102. setOpsRequestContext(c, clientRequestModel, parsed.Stream)
  103. setOpsEndpointContext(c, "", int16(service.RequestTypeFromLegacy(parsed.Stream, false)))
  104. channelMapping, _ := h.gatewayService.ResolveChannelMappingAndRestrict(c.Request.Context(), apiKey.GroupID, routingModel)
  105. if h.errorPassthroughService != nil {
  106. service.BindErrorPassthroughService(c, h.errorPassthroughService)
  107. }
  108. subscription, _ := middleware2.GetSubscriptionFromContext(c)
  109. service.SetOpsLatencyMs(c, service.OpsAuthLatencyMsKey, time.Since(requestStart).Milliseconds())
  110. routingStart := time.Now()
  111. userReleaseFunc, acquired := h.acquireResponsesUserSlot(c, subject.UserID, subject.Concurrency, parsed.Stream, &streamStarted, reqLog)
  112. if !acquired {
  113. return
  114. }
  115. if userReleaseFunc != nil {
  116. defer userReleaseFunc()
  117. }
  118. if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription, service.QuotaPlatform(c.Request.Context(), apiKey)); err != nil {
  119. reqLog.Info("openai.images.billing_eligibility_check_failed", zap.Error(err))
  120. status, code, message, retryAfter := billingErrorDetails(err)
  121. if retryAfter > 0 {
  122. c.Header("Retry-After", strconv.Itoa(retryAfter))
  123. }
  124. h.handleStreamingAwareError(c, status, code, message, streamStarted)
  125. return
  126. }
  127. sessionHash := h.gatewayService.GenerateExplicitSessionHash(c, body)
  128. requestCtx := service.WithOpenAIImagesEndpoint(service.WithOpenAIImageGenerationIntent(c.Request.Context()))
  129. maxAccountSwitches := h.maxAccountSwitches
  130. switchCount := 0
  131. profitVetoCount := 0
  132. failedAccountIDs := make(map[int64]struct{})
  133. sameAccountRetryCount := make(map[int64]int)
  134. var lastFailoverErr *service.UpstreamFailoverError
  135. stopJSONKeepalive := func() {}
  136. jsonKeepaliveStarted := false
  137. defer func() { stopJSONKeepalive() }()
  138. var oauth429FailoverState service.OpenAIOAuth429FailoverState
  139. for {
  140. reqLog.Debug("openai.images.account_selecting", zap.Int("excluded_account_count", len(failedAccountIDs)))
  141. selection, scheduleDecision, err := h.gatewayService.SelectAccountWithSchedulerForImages(
  142. requestCtx,
  143. apiKey.GroupID,
  144. sessionHash,
  145. routingModel,
  146. failedAccountIDs,
  147. parsed.RequiredCapability,
  148. )
  149. if err != nil {
  150. if failoverClientGone(c) {
  151. reqLog.Info("openai.images.account_select_aborted_client_disconnected", zap.Error(err))
  152. return
  153. }
  154. reqLog.Warn("openai.images.account_select_failed",
  155. zap.Error(err),
  156. zap.Int("excluded_account_count", len(failedAccountIDs)),
  157. )
  158. if len(failedAccountIDs) == 0 {
  159. cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, clientRequestModel, routingModel, service.PlatformOpenAI)
  160. if !cls.ModelNotFound {
  161. markOpsRoutingCapacityLimitedIfNoAvailable(c, err)
  162. }
  163. message := cls.Message
  164. if !cls.ModelNotFound {
  165. message = "No available compatible accounts"
  166. }
  167. h.handleStreamingAwareError(c, cls.Status, cls.ErrType, message, streamStarted)
  168. return
  169. }
  170. if lastFailoverErr != nil {
  171. h.handleFailoverExhausted(c, lastFailoverErr, streamStarted)
  172. } else {
  173. h.handleFailoverExhaustedSimple(c, 502, streamStarted)
  174. }
  175. return
  176. }
  177. if selection == nil || selection.Account == nil {
  178. cls := classifyNoAccountErrorFromGin(c, h.gatewayService, apiKey, clientRequestModel, routingModel, service.PlatformOpenAI)
  179. if !cls.ModelNotFound {
  180. markOpsRoutingCapacityLimited(c)
  181. }
  182. message := cls.Message
  183. if !cls.ModelNotFound {
  184. message = "No available compatible accounts"
  185. }
  186. h.handleStreamingAwareError(c, cls.Status, cls.ErrType, message, streamStarted)
  187. return
  188. }
  189. reqLog.Debug("openai.images.account_schedule_decision",
  190. zap.String("layer", scheduleDecision.Layer),
  191. zap.Bool("sticky_session_hit", scheduleDecision.StickySessionHit),
  192. zap.Int("candidate_count", scheduleDecision.CandidateCount),
  193. zap.Int("top_k", scheduleDecision.TopK),
  194. zap.Int64("latency_ms", scheduleDecision.LatencyMs),
  195. zap.Float64("load_skew", scheduleDecision.LoadSkew),
  196. )
  197. account := selection.Account
  198. sessionHash = ensureOpenAIPoolModeSessionHash(sessionHash, account)
  199. reqLog.Debug("openai.images.account_selected", zap.Int64("account_id", account.ID), zap.String("account_name", account.Name))
  200. setOpsSelectedAccount(c, account.ID, account.Platform)
  201. accountReleaseFunc, slotResult := h.acquireResponsesAccountSlot(c, apiKey.GroupID, sessionHash, selection, parsed.Stream, &streamStarted, reqLog)
  202. if slotResult == openAISlotAcquireProfitVetoed {
  203. // Images 调度不装利润门,此分支实际不可达;防御性排除重选并受同一否决上限约束。
  204. if !recordOpenAIProfitVeto(failedAccountIDs, account.ID, &profitVetoCount) {
  205. h.handleOpenAIProfitVetoExhausted(c, streamStarted, reqLog, profitVetoCount)
  206. return
  207. }
  208. continue
  209. }
  210. if slotResult != openAISlotAcquireOK {
  211. return
  212. }
  213. service.SetOpsLatencyMs(c, service.OpsRoutingLatencyMsKey, time.Since(routingStart).Milliseconds())
  214. if !parsed.Stream && !jsonKeepaliveStarted {
  215. stopJSONKeepalive = service.StartOpenAIImagesJSONKeepalive(c, h.openAIImagesJSONKeepaliveInterval())
  216. jsonKeepaliveStarted = true
  217. }
  218. forwardStart := time.Now()
  219. writerSizeBeforeForward := service.OpenAIImagesJSONKeepaliveAdjustedWrittenSize(c)
  220. result, err := func() (*service.OpenAIForwardResult, error) {
  221. defer func() {
  222. if accountReleaseFunc != nil {
  223. accountReleaseFunc()
  224. }
  225. }()
  226. return h.gatewayService.ForwardImages(requestCtx, c, account, body, parsed, channelMapping.MappedModel)
  227. }()
  228. forwardDurationMs := time.Since(forwardStart).Milliseconds()
  229. upstreamLatencyMs, _ := getContextInt64(c, service.OpsUpstreamLatencyMsKey)
  230. responseLatencyMs := forwardDurationMs
  231. if upstreamLatencyMs > 0 && forwardDurationMs > upstreamLatencyMs {
  232. responseLatencyMs = forwardDurationMs - upstreamLatencyMs
  233. }
  234. service.SetOpsLatencyMs(c, service.OpsResponseLatencyMsKey, responseLatencyMs)
  235. if result != nil && result.FirstTokenMs != nil {
  236. service.SetOpsLatencyMs(c, service.OpsTimeToFirstTokenMsKey, int64(*result.FirstTokenMs))
  237. }
  238. if err != nil {
  239. if result != nil && result.ImageCount > 0 {
  240. reqLog.Warn("openai.images.forward_partial_error_with_image_result",
  241. zap.Int64("account_id", account.ID),
  242. zap.Int("image_count", result.ImageCount),
  243. zap.Error(err),
  244. )
  245. } else {
  246. var imageUpstreamErr *service.OpenAIImagesUpstreamError
  247. if errors.As(err, &imageUpstreamErr) {
  248. retryableServerError := service.IsOpenAIImagesRetryableUpstreamError(imageUpstreamErr)
  249. if retryableServerError {
  250. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, requestModel, false, result), false, nil, err)
  251. } else {
  252. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, requestModel, false, result), true, nil)
  253. }
  254. logEvent := "openai.images.upstream_user_error"
  255. if retryableServerError {
  256. logEvent = "openai.images.upstream_server_error_after_flush"
  257. }
  258. reqLog.Warn(logEvent,
  259. zap.Int64("account_id", account.ID),
  260. zap.Int("status_code", imageUpstreamErr.StatusCode),
  261. zap.String("error_type", imageUpstreamErr.ErrorType),
  262. zap.String("error_code", imageUpstreamErr.Code),
  263. zap.Error(err),
  264. )
  265. return
  266. }
  267. var failoverErr *service.UpstreamFailoverError
  268. if errors.As(err, &failoverErr) {
  269. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, requestModel, false, result), false, nil, err)
  270. if service.OpenAIImagesJSONKeepaliveAdjustedWrittenSize(c) != writerSizeBeforeForward {
  271. reqLog.Warn("openai.images.upstream_failover_skipped_after_flush",
  272. zap.Int64("account_id", account.ID),
  273. zap.Int("upstream_status", failoverErr.StatusCode),
  274. )
  275. h.handleFailoverExhausted(c, failoverErr, true)
  276. return
  277. }
  278. if failoverClientGone(c) {
  279. reqLog.Info("openai.images.failover_aborted_client_disconnected",
  280. zap.Int64("account_id", account.ID),
  281. zap.Int("upstream_status", failoverErr.StatusCode),
  282. )
  283. return
  284. }
  285. if failoverErr.RetryableOnSameAccount {
  286. retryLimit := effectiveSameAccountRetryLimit(failoverErr, account)
  287. if sameAccountRetryAllowed(failoverErr, sameAccountRetryCount[account.ID], retryLimit) {
  288. sameAccountRetryCount[account.ID]++
  289. retryDelay := sameAccountRetryDelayFor(failoverErr, sameAccountRetryCount[account.ID])
  290. reqLog.Warn("openai.images.pool_mode_same_account_retry",
  291. zap.Int64("account_id", account.ID),
  292. zap.Int("upstream_status", failoverErr.StatusCode),
  293. zap.Int("retry_limit", retryLimit),
  294. zap.Int("retry_count", sameAccountRetryCount[account.ID]),
  295. zap.Duration("retry_delay", retryDelay),
  296. )
  297. select {
  298. case <-requestCtx.Done():
  299. return
  300. case <-time.After(retryDelay):
  301. }
  302. continue
  303. }
  304. }
  305. h.gatewayService.RecordOpenAIAccountSwitch()
  306. failedAccountIDs[account.ID] = struct{}{}
  307. lastFailoverErr = failoverErr
  308. if switchCount >= maxAccountSwitches {
  309. h.handleFailoverExhausted(c, failoverErr, streamStarted)
  310. return
  311. }
  312. switchCount++
  313. if h.gatewayService.ShouldStopOpenAIOAuth429Failover(account, failoverErr.StatusCode, switchCount, &oauth429FailoverState) {
  314. h.handleFailoverExhausted(c, failoverErr, streamStarted)
  315. return
  316. }
  317. reqLog.Warn("openai.images.upstream_failover_switching",
  318. zap.Int64("account_id", account.ID),
  319. zap.Int("upstream_status", failoverErr.StatusCode),
  320. zap.Int("switch_count", switchCount),
  321. zap.Int("max_switches", maxAccountSwitches),
  322. )
  323. continue
  324. }
  325. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, requestModel, false, result), false, nil, err)
  326. upstreamErrorAlreadyCommunicated := openAIForwardErrorAlreadyCommunicated(c, writerSizeBeforeForward, err)
  327. wroteFallback := false
  328. if !upstreamErrorAlreadyCommunicated {
  329. wroteFallback = h.ensureForwardErrorResponse(c, streamStarted)
  330. }
  331. fields := []zap.Field{
  332. zap.Int64("account_id", account.ID),
  333. zap.Bool("fallback_error_response_written", wroteFallback),
  334. zap.Bool("upstream_error_response_already_written", upstreamErrorAlreadyCommunicated),
  335. zap.Error(err),
  336. }
  337. if shouldLogOpenAIForwardFailureAsWarn(c, wroteFallback) {
  338. reqLog.Warn("openai.images.forward_failed", fields...)
  339. return
  340. }
  341. reqLog.Error("openai.images.forward_failed", fields...)
  342. return
  343. }
  344. }
  345. if result != nil {
  346. // 排除 spark 影子:其 codex_* 仅由 QueryUsage(/wham/usage bengalfox)更新(外审第7轮 P1)。
  347. if account.Type == service.AccountTypeOAuth && !account.IsShadow() {
  348. h.gatewayService.UpdateCodexUsageSnapshotFromHeaders(c.Request.Context(), account.ID, result.ResponseHeaders)
  349. }
  350. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, requestModel, false, result), true, result.FirstTokenMs)
  351. } else {
  352. h.gatewayService.ReportOpenAIAccountScheduleResult(account, openAIAccountScheduleModel(c, account, requestModel, false, result), true, nil)
  353. }
  354. userAgent := c.GetHeader("User-Agent")
  355. clientIP := ip.GetClientIP(c)
  356. requestPayloadHash := service.HashUsageRequestPayload(body)
  357. if parsed.Multipart {
  358. requestPayloadHash = service.HashUsageRequestPayload([]byte(parsed.StickySessionSeed()))
  359. }
  360. inboundEndpoint := GetInboundEndpoint(c)
  361. upstreamEndpoint := GetUpstreamEndpoint(c, account.Platform)
  362. quotaPlatform := service.QuotaPlatform(c.Request.Context(), apiKey)
  363. upstreamModel := ""
  364. if result != nil {
  365. upstreamModel = result.UpstreamModel
  366. }
  367. sessionID := service.ExtractClientSessionID(c)
  368. h.submitMandatoryUsageRecordTask(c.Request.Context(), func(ctx context.Context) {
  369. if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{
  370. Result: result,
  371. APIKey: apiKey,
  372. User: apiKey.User,
  373. Account: account,
  374. Subscription: subscription,
  375. InboundEndpoint: inboundEndpoint,
  376. UpstreamEndpoint: upstreamEndpoint,
  377. UserAgent: userAgent,
  378. IPAddress: clientIP,
  379. RequestPayloadHash: requestPayloadHash,
  380. APIKeyService: h.apiKeyService,
  381. QuotaPlatform: quotaPlatform,
  382. SessionID: sessionID,
  383. ChannelUsageFields: clientRequestedUsageFields(c, channelMapping, requestModel, upstreamModel),
  384. }); err != nil {
  385. logger.L().With(
  386. zap.String("component", "handler.openai_gateway.images"),
  387. zap.Int64("user_id", subject.UserID),
  388. zap.Int64("api_key_id", apiKey.ID),
  389. zap.Any("group_id", apiKey.GroupID),
  390. zap.String("model", clientRequestModel),
  391. zap.Int64("account_id", account.ID),
  392. ).Error("openai.images.record_usage_failed", zap.Error(err))
  393. }
  394. })
  395. reqLog.Debug("openai.images.request_completed",
  396. zap.Int64("account_id", account.ID),
  397. zap.Int("switch_count", switchCount),
  398. )
  399. return
  400. }
  401. }
  402. func (h *OpenAIGatewayHandler) openAIImagesJSONKeepaliveInterval() time.Duration {
  403. if h.cfg == nil || h.cfg.Gateway.ImageNonstreamKeepaliveInterval <= 0 {
  404. return 0
  405. }
  406. return time.Duration(h.cfg.Gateway.ImageNonstreamKeepaliveInterval) * time.Second
  407. }
  408. func isMultipartImagesContentType(contentType string) bool {
  409. return strings.HasPrefix(strings.ToLower(strings.TrimSpace(contentType)), "multipart/form-data")
  410. }