Split ingress keys into text/image protocols with Replicate model listing.
CI / docker (push) Successful in 3m38s
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user