mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:18:29 +08:00
OpenAI groups can mix OAuth accounts with Responses-compatible API key accounts whose upstreams do not implement /models. The manifest handler previously selected one account and fetched its catalog before merging model_mapping aliases, so a permanent 404 returned 502 and hid models the administrator had already configured.\n\nTreat explicit model mappings from schedulable accounts as the authoritative Codex catalog. Build that catalog locally before account selection, preserve group filtering and final-body ETags, and retain the existing upstream discovery and failover path only when no mappings exist. Composite groups keep their existing account-first catalog behavior.
121 lines
4.1 KiB
Go
121 lines
4.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
)
|
|
|
|
// CodexModels serves the Codex models manifest for Codex clients.
|
|
//
|
|
// Codex CLI and the Codex desktop app refresh their model picker from
|
|
// GET {base_url}/models?client_version=... (custom provider mode) or
|
|
// GET /backend-api/codex/models (chatgpt_base_url mode). Both routes land
|
|
// here. Groups with explicit account model mappings are generated locally;
|
|
// otherwise ChatGPT manifests are proxied verbatim and custom API key manifests
|
|
// receive provider-compatibility normalization plus short-lived caching.
|
|
func (h *OpenAIGatewayHandler) CodexModels(c *gin.Context) {
|
|
if c.Request.Context().Err() != nil {
|
|
return
|
|
}
|
|
apiKey, ok := middleware2.GetAPIKeyFromContext(c)
|
|
if !ok || apiKey.Group == nil {
|
|
h.errorResponse(c, http.StatusUnauthorized, "invalid_request_error", "API key group is required")
|
|
return
|
|
}
|
|
if apiKey.Group.Platform != service.PlatformOpenAI && apiKey.Group.Platform != service.PlatformComposite {
|
|
h.errorResponse(c, http.StatusNotFound, "not_found_error", "Codex models manifest is only available for OpenAI and Composite groups")
|
|
return
|
|
}
|
|
|
|
ifNoneMatch := c.GetHeader("If-None-Match")
|
|
configuredManifest, configured, err := h.gatewayService.BuildGroupConfiguredCodexModelsManifest(
|
|
c.Request.Context(),
|
|
apiKey.Group,
|
|
ifNoneMatch,
|
|
)
|
|
if err != nil {
|
|
if c.Request.Context().Err() != nil {
|
|
return
|
|
}
|
|
h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to build Codex models manifest")
|
|
return
|
|
}
|
|
if configured {
|
|
writeCodexModelsManifestResponse(c, configuredManifest)
|
|
return
|
|
}
|
|
|
|
maxAccountSwitches := h.maxAccountSwitches
|
|
if maxAccountSwitches <= 0 {
|
|
maxAccountSwitches = 3
|
|
}
|
|
failedAccountIDs := make(map[int64]struct{})
|
|
switchCount := 0
|
|
var lastUpstreamErr error
|
|
|
|
for {
|
|
account, err := h.gatewayService.SelectAccountForModelWithExclusions(c.Request.Context(), apiKey.GroupID, "", "", failedAccountIDs)
|
|
if err != nil {
|
|
if c.Request.Context().Err() != nil {
|
|
return
|
|
}
|
|
if lastUpstreamErr != nil {
|
|
h.errorResponse(c, infraerrors.Code(lastUpstreamErr), "upstream_error", infraerrors.Message(lastUpstreamErr))
|
|
return
|
|
}
|
|
h.errorResponse(c, http.StatusServiceUnavailable, "upstream_error", "No available OpenAI accounts")
|
|
return
|
|
}
|
|
// 让 ops 错误日志携带实际选中的上游账号,便于定位失效账号(#4544)。
|
|
setOpsSelectedAccount(c, account.ID, account.Platform)
|
|
|
|
// The client ETag represents the final group-specific body, so fetch the
|
|
// source manifest before applying local filtering and alias metadata.
|
|
manifest, err := h.gatewayService.FetchCodexModelsManifest(c.Request.Context(), account, c.Query("client_version"), "")
|
|
if err != nil {
|
|
if c.Request.Context().Err() != nil {
|
|
return
|
|
}
|
|
if service.IsRetryableCodexModelsManifestError(err) && switchCount < maxAccountSwitches {
|
|
failedAccountIDs[account.ID] = struct{}{}
|
|
switchCount++
|
|
lastUpstreamErr = err
|
|
continue
|
|
}
|
|
h.errorResponse(c, infraerrors.Code(err), "upstream_error", infraerrors.Message(err))
|
|
return
|
|
}
|
|
if err := h.gatewayService.CompleteAPIKeyCodexModelsManifestForClient(manifest, account); err != nil {
|
|
h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to complete Codex models manifest")
|
|
return
|
|
}
|
|
if err := h.gatewayService.MergeGroupConfiguredCodexModels(c.Request.Context(), apiKey.Group, manifest, ifNoneMatch); err != nil {
|
|
h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to build Codex models manifest")
|
|
return
|
|
}
|
|
if c.Request.Context().Err() != nil {
|
|
return
|
|
}
|
|
|
|
writeCodexModelsManifestResponse(c, manifest)
|
|
return
|
|
}
|
|
}
|
|
|
|
func writeCodexModelsManifestResponse(c *gin.Context, manifest *service.CodexModelsManifest) {
|
|
if manifest.ETag != "" {
|
|
c.Header("ETag", manifest.ETag)
|
|
}
|
|
if manifest.NotModified {
|
|
c.Status(http.StatusNotModified)
|
|
c.Writer.WriteHeaderNow()
|
|
return
|
|
}
|
|
c.Data(http.StatusOK, "application/json", manifest.Body)
|
|
}
|