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:
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -67,7 +66,7 @@ func (s *Service) Create(ctx context.Context, ingress *model.IngressKey, req Cre
|
||||
if !provider.Enabled {
|
||||
return nil, fmt.Errorf("provider disabled")
|
||||
}
|
||||
input, err := ValidateInput(entry.WorkflowType, req.Input)
|
||||
input, err := ValidateInputWithDefaults(entry.WorkflowType, req.Input, inputDefaultsForEntry(entry))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -85,14 +84,15 @@ func (s *Service) Create(ctx context.Context, ingress *model.IngressKey, req Cre
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ingressModelID := entry.IngressID()
|
||||
inputJSON, _ := json.Marshal(input)
|
||||
p := model.Prediction{
|
||||
ID: id,
|
||||
IngressKeyID: ingress.ID,
|
||||
ProviderID: provider.ID,
|
||||
ModelEntryID: entry.ID,
|
||||
Version: req.Version,
|
||||
Model: entry.ModelID,
|
||||
Version: ingressModelID,
|
||||
Model: ingressModelID,
|
||||
InputJSON: string(inputJSON),
|
||||
Status: model.PredictionStarting,
|
||||
ClientID: "luminary-" + id,
|
||||
@@ -129,6 +129,14 @@ func (s *Service) Create(ctx context.Context, ingress *model.IngressKey, req Cre
|
||||
return s.toAPI(&p), nil
|
||||
}
|
||||
|
||||
func inputDefaultsForEntry(entry model.ModelEntry) map[string]any {
|
||||
defs, err := ParseInputDefaults(entry.InputDefaultsJSON)
|
||||
if err != nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
func (s *Service) resolveModel(version string, ingress *model.IngressKey) (model.ModelEntry, model.Provider, error) {
|
||||
allowedProviders := cache.ParseJSONList(ingress.AllowedProvidersJSON)
|
||||
allowedModels := cache.ParseJSONList(ingress.AllowedModelsJSON)
|
||||
@@ -145,7 +153,7 @@ func (s *Service) resolveModel(version string, ingress *model.IngressKey) (model
|
||||
continue
|
||||
}
|
||||
ingressID := m.IngressID()
|
||||
if !m.MatchesIngressName(version) {
|
||||
if !m.MatchesIngressRef(version, p.Name) {
|
||||
continue
|
||||
}
|
||||
if !cache.MatchesList(allowedModels, ingressID) && !cache.MatchesList(allowedModels, m.ModelID) && !cache.MatchesList(allowedModels, "*") {
|
||||
@@ -157,6 +165,12 @@ func (s *Service) resolveModel(version string, ingress *model.IngressKey) (model
|
||||
if len(candidates) == 0 {
|
||||
return model.ModelEntry{}, model.Provider{}, fmt.Errorf("no model found for version %s", version)
|
||||
}
|
||||
if len(candidates) > 1 && !strings.Contains(version, "/") {
|
||||
return model.ModelEntry{}, model.Provider{}, fmt.Errorf("ambiguous model name %q; use owner/name", version)
|
||||
}
|
||||
if len(candidates) > 1 {
|
||||
return model.ModelEntry{}, model.Provider{}, fmt.Errorf("ambiguous model %q", version)
|
||||
}
|
||||
m := candidates[0]
|
||||
p, ok := s.cache.GetProvider(m.ProviderID)
|
||||
if !ok {
|
||||
@@ -180,15 +194,11 @@ func (s *Service) execute(ctx context.Context, p *model.Prediction, entry model.
|
||||
runInput := cloneMap(input)
|
||||
if entry.WorkflowType == model.WorkflowImg2Img {
|
||||
imageURL, _ := runInput["image"].(string)
|
||||
data, err := downloadURL(ctx, imageURL)
|
||||
data, name, err := s.loadInputImage(ctx, imageURL)
|
||||
if err != nil {
|
||||
s.fail(p, clientIP, start, fmt.Errorf("download image: %w", err))
|
||||
return
|
||||
}
|
||||
name := path.Base(imageURL)
|
||||
if name == "" || name == "." || name == "/" {
|
||||
name = "input.png"
|
||||
}
|
||||
uploaded, err := client.UploadImage(ctx, data, name)
|
||||
if err != nil {
|
||||
s.fail(p, clientIP, start, fmt.Errorf("upload image: %w", err))
|
||||
@@ -409,6 +419,26 @@ func formatTime(t *time.Time) any {
|
||||
return t.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
func (s *Service) loadInputImage(ctx context.Context, imageURL string) ([]byte, string, error) {
|
||||
if id, ok := parseGatewayFileID(imageURL); ok && s.images != nil {
|
||||
data, img, err := s.images.ReadContent(id)
|
||||
if err == nil {
|
||||
if err := validateImageBytes(data); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return data, imageFilenameFromStored(img.Filename, img.Mime, data), nil
|
||||
}
|
||||
}
|
||||
data, err := downloadURL(ctx, imageURL)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err := validateImageBytes(data); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return data, imageFilenameFromURL(imageURL, data), nil
|
||||
}
|
||||
|
||||
func downloadURL(ctx context.Context, raw string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, raw, nil)
|
||||
if err != nil {
|
||||
@@ -455,7 +485,7 @@ func (s *Service) recordUsage(ingress *model.IngressKey, provider *model.Provide
|
||||
ProviderID: providerID,
|
||||
ClientIP: clientIP,
|
||||
Endpoint: model.EndpointPrediction,
|
||||
Model: p.ID,
|
||||
Model: p.Model,
|
||||
LatencyMs: time.Since(start).Milliseconds(),
|
||||
Status: status,
|
||||
ErrorMsg: errMsg,
|
||||
|
||||
Reference in New Issue
Block a user