openai_codex_models_handler.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. package handler
  2. import (
  3. "errors"
  4. "net/http"
  5. "github.com/gin-gonic/gin"
  6. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  7. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  8. "github.com/Wei-Shaw/sub2api/internal/service"
  9. )
  10. // CodexModels serves the Codex models manifest for Codex clients.
  11. //
  12. // Codex CLI and the Codex desktop app refresh their model picker from
  13. // GET {base_url}/models?client_version=... (custom provider mode) or
  14. // GET /backend-api/codex/models (chatgpt_base_url mode). Both routes land
  15. // here. Pinned discovery takes precedence over local account model mappings;
  16. // when disabled, groups with explicit mappings are generated locally;
  17. // otherwise ChatGPT manifests are proxied verbatim and custom API key manifests
  18. // receive provider-compatibility normalization plus short-lived caching.
  19. func (h *OpenAIGatewayHandler) CodexModels(c *gin.Context) {
  20. if c.Request.Context().Err() != nil {
  21. return
  22. }
  23. apiKey, ok := middleware2.GetAPIKeyFromContext(c)
  24. if !ok || apiKey.Group == nil {
  25. h.errorResponse(c, http.StatusUnauthorized, "invalid_request_error", "API key group is required")
  26. return
  27. }
  28. if apiKey.Group.Platform != service.PlatformOpenAI && apiKey.Group.Platform != service.PlatformComposite {
  29. h.errorResponse(c, http.StatusNotFound, "not_found_error", "Codex models manifest is only available for OpenAI and Composite groups")
  30. return
  31. }
  32. ifNoneMatch := c.GetHeader("If-None-Match")
  33. // 固定账号分支:开启后只用选定账号拉取 manifest,不经过调度器;
  34. // 全部不可用/全部失败时按 FallbackToScheduler 决定回退调度器或返回错误。
  35. if apiKey.Group.Platform == service.PlatformOpenAI &&
  36. apiKey.Group.CodexModelsManifestConfig.Enabled {
  37. pinnedManifest, pinnedAccount, pinnedErr := h.gatewayService.FetchPinnedCodexModelsManifest(
  38. c.Request.Context(),
  39. apiKey.Group,
  40. c.Query("client_version"),
  41. )
  42. if pinnedErr != nil {
  43. if c.Request.Context().Err() != nil {
  44. return
  45. }
  46. if !apiKey.Group.CodexModelsManifestConfig.FallbackToScheduler {
  47. if errors.Is(pinnedErr, service.ErrNoPinnedCodexModelsAccounts) {
  48. h.errorResponse(c, http.StatusServiceUnavailable, "upstream_error", "No available pinned OpenAI accounts")
  49. return
  50. }
  51. h.errorResponse(c, infraerrors.Code(pinnedErr), "upstream_error", infraerrors.Message(pinnedErr))
  52. return
  53. }
  54. // 回退开启:跌入下方调度器循环。
  55. } else {
  56. // 让 ops 错误日志携带实际拉取成功的首个固定账号。
  57. setOpsSelectedAccount(c, pinnedAccount.ID, pinnedAccount.Platform)
  58. if err := h.gatewayService.MergeGroupConfiguredCodexModels(c.Request.Context(), apiKey.Group, pinnedManifest, ifNoneMatch); err != nil {
  59. h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to build Codex models manifest")
  60. return
  61. }
  62. if c.Request.Context().Err() != nil {
  63. return
  64. }
  65. writeOpenAIModelsResponse(c, pinnedManifest)
  66. return
  67. }
  68. }
  69. if !apiKey.Group.CodexModelsManifestConfig.Enabled {
  70. configuredManifest, configured, err := h.gatewayService.BuildGroupConfiguredCodexModelsManifest(
  71. c.Request.Context(),
  72. apiKey.Group,
  73. ifNoneMatch,
  74. )
  75. if err != nil {
  76. if c.Request.Context().Err() != nil {
  77. return
  78. }
  79. h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to build Codex models manifest")
  80. return
  81. }
  82. if configured {
  83. writeOpenAIModelsResponse(c, configuredManifest)
  84. return
  85. }
  86. }
  87. maxAccountSwitches := h.maxAccountSwitches
  88. if maxAccountSwitches <= 0 {
  89. maxAccountSwitches = 3
  90. }
  91. failedAccountIDs := make(map[int64]struct{})
  92. switchCount := 0
  93. var lastUpstreamErr error
  94. for {
  95. account, err := h.gatewayService.SelectAccountForModelWithExclusions(c.Request.Context(), apiKey.GroupID, "", "", failedAccountIDs)
  96. if err != nil {
  97. if c.Request.Context().Err() != nil {
  98. return
  99. }
  100. if lastUpstreamErr != nil {
  101. h.errorResponse(c, infraerrors.Code(lastUpstreamErr), "upstream_error", infraerrors.Message(lastUpstreamErr))
  102. return
  103. }
  104. h.errorResponse(c, http.StatusServiceUnavailable, "upstream_error", "No available OpenAI accounts")
  105. return
  106. }
  107. // 让 ops 错误日志携带实际选中的上游账号,便于定位失效账号(#4544)。
  108. setOpsSelectedAccount(c, account.ID, account.Platform)
  109. // The client ETag represents the final group-specific body, so fetch the
  110. // source manifest before applying local filtering and alias metadata.
  111. manifest, err := h.gatewayService.FetchCodexModelsManifest(c.Request.Context(), account, c.Query("client_version"), "")
  112. if err != nil {
  113. if c.Request.Context().Err() != nil {
  114. return
  115. }
  116. if service.IsRetryableCodexModelsManifestError(err) && switchCount < maxAccountSwitches {
  117. failedAccountIDs[account.ID] = struct{}{}
  118. switchCount++
  119. lastUpstreamErr = err
  120. continue
  121. }
  122. h.errorResponse(c, infraerrors.Code(err), "upstream_error", infraerrors.Message(err))
  123. return
  124. }
  125. if err := h.gatewayService.CompleteAPIKeyCodexModelsManifestForClient(manifest, account); err != nil {
  126. h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to complete Codex models manifest")
  127. return
  128. }
  129. if err := service.ApplyPinnedCodexModelsMapping(manifest, account, apiKey.Group); err != nil {
  130. h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to apply model mappings")
  131. return
  132. }
  133. if err := h.gatewayService.MergeGroupConfiguredCodexModels(c.Request.Context(), apiKey.Group, manifest, ifNoneMatch); err != nil {
  134. h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to build Codex models manifest")
  135. return
  136. }
  137. if c.Request.Context().Err() != nil {
  138. return
  139. }
  140. writeOpenAIModelsResponse(c, manifest)
  141. return
  142. }
  143. }