2026-09-05 19:58:47 +08:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
2026-09-10 15:57:17 +08:00
|
|
|
"encoding/json"
|
2026-09-05 19:58:47 +08:00
|
|
|
"errors"
|
2026-09-10 15:57:17 +08:00
|
|
|
"fmt"
|
2026-09-05 19:58:47 +08:00
|
|
|
"net/http"
|
|
|
|
|
|
|
|
|
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (h *GatewayHandler) pinnedOpenAIModels(c *gin.Context, group *service.Group) {
|
|
|
|
|
if c.Request.Context().Err() != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if h.openAIGatewayService == nil {
|
|
|
|
|
writeOpenAIModelsError(c, http.StatusInternalServerError, "api_error", "OpenAI model discovery is not configured")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-10 15:57:17 +08:00
|
|
|
etag := c.GetHeader("If-None-Match")
|
|
|
|
|
if c.Param("model") != "" {
|
|
|
|
|
etag = "" // A collection ETag cannot validate a single-model representation.
|
|
|
|
|
}
|
2026-09-05 19:58:47 +08:00
|
|
|
response, account, err := h.openAIGatewayService.FetchPinnedOpenAIModelsList(
|
2026-09-10 15:57:17 +08:00
|
|
|
c.Request.Context(), group, h.maxAccountSwitches, etag,
|
2026-09-05 19:58:47 +08:00
|
|
|
)
|
|
|
|
|
if c.Request.Context().Err() != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, service.ErrNoPinnedCodexModelsAccounts) {
|
|
|
|
|
writeOpenAIModelsError(c, http.StatusServiceUnavailable, "upstream_error", "No available OpenAI model discovery accounts")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeOpenAIModelsError(c, infraerrors.Code(err), "upstream_error", infraerrors.Message(err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
setOpsSelectedAccount(c, account.ID, account.Platform)
|
|
|
|
|
writeOpenAIModelsResponse(c, response)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeOpenAIModelsError(c *gin.Context, status int, errorType, message string) {
|
|
|
|
|
c.JSON(status, gin.H{"error": gin.H{"type": errorType, "message": message}})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeOpenAIModelsResponse(c *gin.Context, manifest *service.OpenAIModelsResponse) {
|
2026-09-10 15:57:17 +08:00
|
|
|
if c.Param("model") != "" {
|
|
|
|
|
writeRetrievedModel(c, manifest.Body)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-05 19:58:47 +08:00
|
|
|
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)
|
|
|
|
|
}
|
2026-09-10 15:57:17 +08:00
|
|
|
|
|
|
|
|
// Both discovery endpoints consume the same final catalogue, after group/platform
|
|
|
|
|
// selection and allowlist filtering. Preserve every field on the selected entry.
|
|
|
|
|
func writeModelsListResponse(c *gin.Context, models any) {
|
|
|
|
|
response := gin.H{"object": "list", "data": models}
|
|
|
|
|
if c.Param("model") == "" {
|
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
body, err := json.Marshal(response)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeOpenAIModelsError(c, http.StatusInternalServerError, "api_error", "Failed to encode model catalogue")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeRetrievedModel(c, body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeRetrievedModel(c *gin.Context, body []byte) {
|
|
|
|
|
var catalog struct {
|
|
|
|
|
Data []json.RawMessage `json:"data"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(body, &catalog); err != nil {
|
|
|
|
|
writeOpenAIModelsError(c, http.StatusBadGateway, "upstream_error", "Invalid model catalogue")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
modelID := c.Param("model")
|
|
|
|
|
for _, raw := range catalog.Data {
|
|
|
|
|
var model map[string]json.RawMessage
|
|
|
|
|
if err := json.Unmarshal(raw, &model); err != nil {
|
|
|
|
|
writeOpenAIModelsError(c, http.StatusBadGateway, "upstream_error", "Invalid model catalogue entry")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var id string
|
|
|
|
|
if err := json.Unmarshal(model["id"], &id); err != nil {
|
|
|
|
|
writeOpenAIModelsError(c, http.StatusBadGateway, "upstream_error", "Invalid model catalogue ID")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if id == modelID {
|
|
|
|
|
c.Data(http.StatusOK, "application/json", raw)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": gin.H{
|
|
|
|
|
"type": "invalid_request_error", "code": "model_not_found", "param": "model",
|
|
|
|
|
"message": fmt.Sprintf("Model %q does not exist or is not available for this group", modelID),
|
|
|
|
|
}})
|
|
|
|
|
}
|