Split ingress keys into text/image protocols with Replicate model listing.
CI / docker (push) Successful in 3m38s

Add model input defaults (including prompt), workflow model editing, ingress API docs, and fix img2img image upload filenames for signed URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
renjue
2026-06-29 21:41:06 +08:00
parent 2665ac1dee
commit 321683f863
35 changed files with 1646 additions and 167 deletions
+32 -14
View File
@@ -19,18 +19,27 @@ func (h *Handler) ListIngressKeys(c *gin.Context) {
func (h *Handler) CreateIngressKey(c *gin.Context) {
var req struct {
Name string `json:"name"`
BudgetLimit float64 `json:"budget_limit"`
RateLimitRPM int `json:"rate_limit_rpm"`
RateLimitTPM int `json:"rate_limit_tpm"`
AllowedProviders []string `json:"allowed_providers"`
AllowedModels []string `json:"allowed_models"`
ExpiresAt *time.Time `json:"expires_at"`
Name string `json:"name"`
Protocol model.IngressKeyProtocol `json:"protocol"`
BudgetLimit float64 `json:"budget_limit"`
RateLimitRPM int `json:"rate_limit_rpm"`
RateLimitTPM int `json:"rate_limit_tpm"`
AllowedProviders []string `json:"allowed_providers"`
AllowedModels []string `json:"allowed_models"`
ExpiresAt *time.Time `json:"expires_at"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.Name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
protocol := req.Protocol
if protocol == "" {
protocol = model.IngressProtocolText
}
if !protocol.Valid() {
c.JSON(http.StatusBadRequest, gin.H{"error": "protocol must be text or image"})
return
}
plainKey, err := generateIngressKey()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "key generation failed"})
@@ -57,6 +66,7 @@ func (h *Handler) CreateIngressKey(c *gin.Context) {
Name: req.Name,
KeyHash: hash,
Prefix: plainKey[:12] + "...",
Protocol: protocol,
Enabled: true,
BudgetLimit: req.BudgetLimit,
RateLimitRPM: req.RateLimitRPM,
@@ -91,18 +101,26 @@ func (h *Handler) UpdateIngressKey(c *gin.Context) {
return
}
var req struct {
Name string `json:"name"`
Enabled *bool `json:"enabled"`
BudgetLimit *float64 `json:"budget_limit"`
RateLimitRPM *int `json:"rate_limit_rpm"`
RateLimitTPM *int `json:"rate_limit_tpm"`
AllowedProviders []string `json:"allowed_providers"`
AllowedModels []string `json:"allowed_models"`
Name string `json:"name"`
Protocol *model.IngressKeyProtocol `json:"protocol"`
Enabled *bool `json:"enabled"`
BudgetLimit *float64 `json:"budget_limit"`
RateLimitRPM *int `json:"rate_limit_rpm"`
RateLimitTPM *int `json:"rate_limit_tpm"`
AllowedProviders []string `json:"allowed_providers"`
AllowedModels []string `json:"allowed_models"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if req.Protocol != nil {
if !req.Protocol.Valid() {
c.JSON(http.StatusBadRequest, gin.H{"error": "protocol must be text or image"})
return
}
k.Protocol = *req.Protocol
}
if req.Name != "" {
k.Name = req.Name
}
+47 -12
View File
@@ -227,8 +227,10 @@ func (h *Handler) CreateModel(c *gin.Context) {
DisplayName string `json:"display_name"`
WorkflowType model.WorkflowType `json:"workflow_type"`
WorkflowJSON string `json:"workflow_json"`
InputBindingJSON string `json:"input_binding_json"`
InputBinding map[string]*prediction.NodeField `json:"input_binding"`
InputBindingJSON string `json:"input_binding_json"`
InputBinding map[string]*prediction.NodeField `json:"input_binding"`
InputDefaultsJSON string `json:"input_defaults_json"`
InputDefaults map[string]any `json:"input_defaults"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.ModelID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
@@ -260,19 +262,25 @@ func (h *Handler) CreateModel(c *gin.Context) {
b, _ := json.Marshal(req.InputBinding)
bindingJSON = string(b)
}
defaultsJSON, err := marshalInputDefaults(req.InputDefaultsJSON, req.InputDefaults)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
wt := req.WorkflowType
if wt == "" {
wt = model.WorkflowTxt2Img
}
m := model.ModelEntry{
ProviderID: providerID,
ModelID: req.ModelID,
Alias: normalizeAlias(req.Alias),
DisplayName: req.DisplayName,
WorkflowType: wt,
WorkflowJSON: req.WorkflowJSON,
InputBindingJSON: bindingJSON,
VersionHash: prediction.HashWorkflowJSON(req.WorkflowJSON),
ProviderID: providerID,
ModelID: req.ModelID,
Alias: normalizeAlias(req.Alias),
DisplayName: req.DisplayName,
WorkflowType: wt,
WorkflowJSON: req.WorkflowJSON,
InputBindingJSON: bindingJSON,
InputDefaultsJSON: defaultsJSON,
VersionHash: prediction.HashWorkflowJSON(req.WorkflowJSON),
Enabled: true,
}
if prov.Category == model.CategoryImage {
@@ -301,8 +309,10 @@ func (h *Handler) UpdateModel(c *gin.Context) {
ModelID string `json:"model_id"`
WorkflowType model.WorkflowType `json:"workflow_type"`
WorkflowJSON string `json:"workflow_json"`
InputBindingJSON string `json:"input_binding_json"`
InputBinding map[string]*prediction.NodeField `json:"input_binding"`
InputBindingJSON string `json:"input_binding_json"`
InputBinding map[string]*prediction.NodeField `json:"input_binding"`
InputDefaultsJSON string `json:"input_defaults_json"`
InputDefaults map[string]any `json:"input_defaults"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
@@ -349,6 +359,14 @@ func (h *Handler) UpdateModel(c *gin.Context) {
b, _ := json.Marshal(req.InputBinding)
m.InputBindingJSON = string(b)
}
if req.InputDefaults != nil || req.InputDefaultsJSON != "" {
defaultsJSON, err := marshalInputDefaults(req.InputDefaultsJSON, req.InputDefaults)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
m.InputDefaultsJSON = defaultsJSON
}
prov, _ := h.cache.GetProvider(m.ProviderID)
if prov.Category == model.CategoryImage {
m.Alias = ""
@@ -422,3 +440,20 @@ func (h *Handler) DeleteModel(c *gin.Context) {
h.cache.SetModels(m.ProviderID, filtered)
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func marshalInputDefaults(raw string, defaults map[string]any) (string, error) {
if defaults != nil {
b, err := json.Marshal(defaults)
if err != nil {
return "", err
}
return string(b), nil
}
if raw == "" {
return "{}", nil
}
if _, err := prediction.ParseInputDefaults(raw); err != nil {
return "", err
}
return raw, nil
}
-15
View File
@@ -21,7 +21,6 @@ func New(gw *gateway.Service) *Handler {
}
func (h *Handler) Register(r *gin.RouterGroup) {
r.GET("/models", h.ListModels)
r.POST("/chat/completions", h.forward(provider.EndpointChatCompletion))
r.POST("/completions", h.forward(provider.EndpointTextCompletion))
r.POST("/responses", h.forward(provider.EndpointResponses))
@@ -38,20 +37,6 @@ func (h *Handler) ingressKey(c *gin.Context) (*model.IngressKey, bool) {
return k, ok
}
func (h *Handler) ListModels(c *gin.Context) {
ingress, ok := h.ingressKey(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
models, err := h.gateway.ListModels(c.Request.Context(), ingress)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"object": "list", "data": models})
}
func (h *Handler) forward(endpoint provider.EndpointType) gin.HandlerFunc {
return func(c *gin.Context) {
ingress, ok := h.ingressKey(c)
+57
View File
@@ -0,0 +1,57 @@
package v1handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/rose_cat707/luminary/internal/gateway"
"github.com/rose_cat707/luminary/internal/model"
"github.com/rose_cat707/luminary/internal/prediction"
)
type ModelsHandler struct {
gateway *gateway.Service
predictions *prediction.Service
baseURL func() string
}
func NewModelsHandler(gw *gateway.Service, pred *prediction.Service, baseURL func() string) *ModelsHandler {
return &ModelsHandler{gateway: gw, predictions: pred, baseURL: baseURL}
}
func (h *ModelsHandler) List(c *gin.Context) {
ingress, ok := ingressKey(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
switch ingress.EffectiveProtocol() {
case model.IngressProtocolImage:
results, err := h.predictions.ListPublicModels(c.Request.Context(), ingress, h.baseURL())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"previous": nil,
"next": nil,
"results": results,
})
default:
models, err := h.gateway.ListModels(c.Request.Context(), ingress)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"object": "list", "data": models})
}
}
func ingressKey(c *gin.Context) (*model.IngressKey, bool) {
v, ok := c.Get("ingress_key")
if !ok {
return nil, false
}
k, ok := v.(*model.IngressKey)
return k, ok
}