增加模型别名
This commit is contained in:
@@ -54,6 +54,10 @@ func (s *AdminReadService) Models(ctx context.Context) ([]model.ModelConfig, err
|
||||
return s.models.List(ctx)
|
||||
}
|
||||
|
||||
func (s *AdminReadService) ModelNameMap(ctx context.Context) (map[string]string, error) {
|
||||
return s.models.NameMap(ctx)
|
||||
}
|
||||
|
||||
func (s *AdminReadService) ModelsView(ctx context.Context) ([]map[string]any, error) {
|
||||
items, err := s.models.List(ctx)
|
||||
if err != nil {
|
||||
@@ -63,6 +67,7 @@ func (s *AdminReadService) ModelsView(ctx context.Context) ([]map[string]any, er
|
||||
for _, item := range items {
|
||||
out = append(out, map[string]any{
|
||||
"id": item.ID,
|
||||
"alias": item.Alias,
|
||||
"type": item.Type,
|
||||
"name": item.Name,
|
||||
"provider": item.Provider,
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
// row does not exist, so handlers can translate it into a 404 (GORM's Delete
|
||||
// does not error on a zero-row delete).
|
||||
var ErrNotFound = errors.New("not found")
|
||||
var ErrModelAliasCollision = errors.New("model alias collision")
|
||||
|
||||
type AdminWriteService struct {
|
||||
users *repo.UserRepository
|
||||
@@ -94,18 +95,18 @@ func (s *AdminWriteService) CreateUser(ctx context.Context, body map[string]any)
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
ID: "u-" + uuid.NewString()[:10],
|
||||
Email: email,
|
||||
Name: name,
|
||||
PasswordHash: passwordHash,
|
||||
Role: role,
|
||||
Status: status,
|
||||
Credits: credits,
|
||||
Notes: notes,
|
||||
ID: "u-" + uuid.NewString()[:10],
|
||||
Email: email,
|
||||
Name: name,
|
||||
PasswordHash: passwordHash,
|
||||
Role: role,
|
||||
Status: status,
|
||||
Credits: credits,
|
||||
Notes: notes,
|
||||
ConcurrencyGroupID: cgroupID,
|
||||
InviteCode: randomInviteCode(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
InviteCode: randomInviteCode(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := s.users.Create(ctx, user); err != nil {
|
||||
return nil, err
|
||||
@@ -347,6 +348,7 @@ func (s *AdminWriteService) CreateModel(ctx context.Context, body map[string]any
|
||||
modelID := strings.TrimSpace(stringValue(body["id"]))
|
||||
modelType := normalizedModelType(stringValue(body["type"]))
|
||||
provider := strings.TrimSpace(stringValue(body["provider"]))
|
||||
alias := strings.TrimSpace(stringValue(body["alias"]))
|
||||
if modelID == "" {
|
||||
return nil, errors.New("id required")
|
||||
}
|
||||
@@ -356,6 +358,9 @@ func (s *AdminWriteService) CreateModel(ctx context.Context, body map[string]any
|
||||
if provider == "" {
|
||||
return nil, errors.New("provider required")
|
||||
}
|
||||
if err := s.validateModelNameSpace(ctx, "", modelID, alias); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prices := jsonMap(body["prices"])
|
||||
// image: tiers derive from the price keys (form omits resolutions);
|
||||
@@ -366,24 +371,25 @@ func (s *AdminWriteService) CreateModel(ctx context.Context, body map[string]any
|
||||
}
|
||||
|
||||
item := &model.ModelConfig{
|
||||
ID: modelID,
|
||||
Type: modelType,
|
||||
Name: defaultString(strings.TrimSpace(stringValue(body["name"])), modelID),
|
||||
Provider: provider,
|
||||
Enabled: boolValueWithDefault(body["enabled"], true),
|
||||
Ratios: jsonArray(body["ratios"]),
|
||||
Prices: prices,
|
||||
Resolutions: resolutions,
|
||||
ImageToImage: boolValueWithDefault(body["image_to_image"], false),
|
||||
DurationPrices: jsonMap(body["duration_prices"]),
|
||||
ID: modelID,
|
||||
Type: modelType,
|
||||
Name: defaultString(strings.TrimSpace(stringValue(body["name"])), modelID),
|
||||
Alias: alias,
|
||||
Provider: provider,
|
||||
Enabled: boolValueWithDefault(body["enabled"], true),
|
||||
Ratios: jsonArray(body["ratios"]),
|
||||
Prices: prices,
|
||||
Resolutions: resolutions,
|
||||
ImageToImage: boolValueWithDefault(body["image_to_image"], false),
|
||||
DurationPrices: jsonMap(body["duration_prices"]),
|
||||
PricesAgent: jsonMap(body["prices_agent"]),
|
||||
DurationPricesAgent: jsonMap(body["duration_prices_agent"]),
|
||||
Durations: jsonArray(body["durations"]),
|
||||
MaxReferenceImages: intValue(body["max_reference_images"]),
|
||||
ReferenceMode: defaultString(strings.TrimSpace(stringValue(body["reference_mode"])), "none"),
|
||||
Weight: intValue(body["weight"]),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Durations: jsonArray(body["durations"]),
|
||||
MaxReferenceImages: intValue(body["max_reference_images"]),
|
||||
ReferenceMode: defaultString(strings.TrimSpace(stringValue(body["reference_mode"])), "none"),
|
||||
Weight: intValue(body["weight"]),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := s.models.Create(ctx, item); err != nil {
|
||||
return nil, err
|
||||
@@ -393,6 +399,7 @@ func (s *AdminWriteService) CreateModel(ctx context.Context, body map[string]any
|
||||
|
||||
func (s *AdminWriteService) UpdateModel(ctx context.Context, modelID string, body map[string]any) (*model.ModelConfig, error) {
|
||||
patch := map[string]any{}
|
||||
alias := ""
|
||||
if _, ok := body["type"]; ok {
|
||||
modelType := normalizedModelType(stringValue(body["type"]))
|
||||
if modelType == "" {
|
||||
@@ -403,6 +410,13 @@ func (s *AdminWriteService) UpdateModel(ctx context.Context, modelID string, bod
|
||||
if _, ok := body["name"]; ok {
|
||||
patch["name"] = strings.TrimSpace(stringValue(body["name"]))
|
||||
}
|
||||
if _, ok := body["alias"]; ok {
|
||||
alias = strings.TrimSpace(stringValue(body["alias"]))
|
||||
if err := s.validateModelNameSpace(ctx, modelID, modelID, alias); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patch["alias"] = alias
|
||||
}
|
||||
if _, ok := body["provider"]; ok {
|
||||
provider := strings.TrimSpace(stringValue(body["provider"]))
|
||||
if provider == "" {
|
||||
@@ -457,6 +471,36 @@ func (s *AdminWriteService) UpdateModel(ctx context.Context, modelID string, bod
|
||||
return s.models.Update(ctx, modelID, patch)
|
||||
}
|
||||
|
||||
func (s *AdminWriteService) validateModelNameSpace(ctx context.Context, selfID, candidateID, candidateAlias string) error {
|
||||
candidateID = strings.TrimSpace(candidateID)
|
||||
candidateAlias = strings.TrimSpace(candidateAlias)
|
||||
if candidateID == "" && candidateAlias == "" {
|
||||
return nil
|
||||
}
|
||||
items, err := s.models.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
if item.ID == selfID {
|
||||
continue
|
||||
}
|
||||
existingAlias := strings.TrimSpace(item.Alias)
|
||||
if candidateID != "" && existingAlias == candidateID {
|
||||
return fmt.Errorf("%w: model id %q collides with existing alias %q", ErrModelAliasCollision, candidateID, existingAlias)
|
||||
}
|
||||
if candidateAlias != "" {
|
||||
if item.ID == candidateAlias {
|
||||
return fmt.Errorf("%w: alias %q collides with existing model id %q", ErrModelAliasCollision, candidateAlias, item.ID)
|
||||
}
|
||||
if existingAlias != "" && existingAlias == candidateAlias {
|
||||
return fmt.Errorf("%w: alias %q collides with existing alias %q", ErrModelAliasCollision, candidateAlias, existingAlias)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AdminWriteService) DeleteModel(ctx context.Context, modelID string) error {
|
||||
rows, err := s.models.Delete(ctx, modelID)
|
||||
if err != nil {
|
||||
|
||||
@@ -124,6 +124,10 @@ func (s *UserGenerationService) MyJobs(ctx context.Context, user *model.User, so
|
||||
if source != "admin" {
|
||||
source = "user"
|
||||
}
|
||||
modelNames, err := s.ModelNameMap(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pending, err := s.events.PendingByUser(ctx, user.ID, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -133,12 +137,16 @@ func (s *UserGenerationService) MyJobs(ctx context.Context, user *model.User, so
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"pending": shapeJobEvent(pending),
|
||||
"latest": shapeJobEvent(latest),
|
||||
"pending": shapeJobEvent(pending, modelNames),
|
||||
"latest": shapeJobEvent(latest, modelNames),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func shapeJobEvent(item *model.EventLog) map[string]any {
|
||||
func (s *UserGenerationService) ModelNameMap(ctx context.Context) (map[string]string, error) {
|
||||
return s.models.NameMap(ctx)
|
||||
}
|
||||
|
||||
func shapeJobEvent(item *model.EventLog, modelNames map[string]string) map[string]any {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -150,7 +158,7 @@ func shapeJobEvent(item *model.EventLog) map[string]any {
|
||||
return map[string]any{
|
||||
"id": item.ID,
|
||||
"kind": item.Kind,
|
||||
"model": item.Model,
|
||||
"model": displayModelName(modelNames, item.Model),
|
||||
"prompt": item.Prompt,
|
||||
"ratio": item.Ratio,
|
||||
"resolution": item.Resolution,
|
||||
@@ -167,6 +175,19 @@ func shapeJobEvent(item *model.EventLog) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func displayModelName(modelNames map[string]string, raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if modelNames != nil {
|
||||
if name, ok := modelNames[raw]; ok && strings.TrimSpace(name) != "" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// referenceURLs turns the stored relative reference paths into /images URLs so
|
||||
// the playground can re-display the uploaded reference image(s) after a reload.
|
||||
func referenceURLs(raw []byte) []string {
|
||||
|
||||
@@ -299,7 +299,7 @@ func (s *V1Service) ListModels(ctx context.Context) ([]map[string]any, error) {
|
||||
continue
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"id": item.ID,
|
||||
"id": item.EffectiveName(),
|
||||
"object": "model",
|
||||
"created": now,
|
||||
"owned_by": item.Provider,
|
||||
@@ -537,7 +537,7 @@ func (s *V1Service) prepareImageExecution(ctx context.Context, principal *APIPri
|
||||
return map[string]any{
|
||||
"created": time.Now().Unix(),
|
||||
"data": []map[string]any{{"b64_json": b64}},
|
||||
"model": modelItem.ID,
|
||||
"model": modelItem.EffectiveName(),
|
||||
"provider": modelItem.Provider,
|
||||
"kind": "image",
|
||||
"b64_json": b64,
|
||||
@@ -549,7 +549,7 @@ func (s *V1Service) prepareImageExecution(ctx context.Context, principal *APIPri
|
||||
return map[string]any{
|
||||
"created": time.Now().Unix(),
|
||||
"data": []map[string]any{{"url": fileURL, "b64_json": nil}},
|
||||
"model": modelItem.ID,
|
||||
"model": modelItem.EffectiveName(),
|
||||
"provider": modelItem.Provider,
|
||||
"kind": "image",
|
||||
"url": fileURL,
|
||||
@@ -681,7 +681,7 @@ func (s *V1Service) prepareVideoExecution(ctx context.Context, principal *APIPri
|
||||
return map[string]any{
|
||||
"created": time.Now().Unix(),
|
||||
"data": []map[string]any{{"b64_json": b64}},
|
||||
"model": modelItem.ID,
|
||||
"model": modelItem.EffectiveName(),
|
||||
"provider": modelItem.Provider,
|
||||
"kind": "video",
|
||||
"b64_json": b64,
|
||||
@@ -693,7 +693,7 @@ func (s *V1Service) prepareVideoExecution(ctx context.Context, principal *APIPri
|
||||
return map[string]any{
|
||||
"created": time.Now().Unix(),
|
||||
"data": []map[string]any{{"url": fileURL}},
|
||||
"model": modelItem.ID,
|
||||
"model": modelItem.EffectiveName(),
|
||||
"provider": modelItem.Provider,
|
||||
"kind": "video",
|
||||
"url": fileURL,
|
||||
@@ -725,7 +725,7 @@ func (s *V1Service) StartVideoJob(ctx context.Context, principal *APIPrincipal,
|
||||
return nil, err
|
||||
}
|
||||
go s.runVideoJob(ctx, principal, in, modelItem, eventID, aspectRatio, resolution, duration, price, refFiles)
|
||||
return videoJobObject(eventID, modelItem.ID, "queued", 0, duration, sizeFromRatioRes(aspectRatio, resolution), time.Now().Unix(), 0, ""), nil
|
||||
return videoJobObject(eventID, modelItem.EffectiveName(), "queued", 0, duration, sizeFromRatioRes(aspectRatio, resolution), time.Now().Unix(), 0, ""), nil
|
||||
}
|
||||
|
||||
// runVideoJob renders the clip in the background, capturing the upstream URL
|
||||
@@ -792,7 +792,13 @@ func (s *V1Service) VideoJob(ctx context.Context, principal *APIPrincipal, id st
|
||||
if ev.Status == "failed" {
|
||||
errMsg = ev.Error
|
||||
}
|
||||
return videoJobObject(ev.ID, ev.Model, status, progress, ev.Duration, sizeFromRatioRes(ev.Ratio, ev.Resolution), ev.TS.Unix(), completedAt, errMsg), nil
|
||||
modelName := ev.Model
|
||||
if nameByID, nerr := s.models.NameMap(ctx); nerr == nil {
|
||||
if name, ok := nameByID[ev.Model]; ok && strings.TrimSpace(name) != "" {
|
||||
modelName = name
|
||||
}
|
||||
}
|
||||
return videoJobObject(ev.ID, modelName, status, progress, ev.Duration, sizeFromRatioRes(ev.Ratio, ev.Resolution), ev.TS.Unix(), completedAt, errMsg), nil
|
||||
}
|
||||
|
||||
// OpenVideoContent streams a completed job's video by proxying the stored
|
||||
|
||||
Reference in New Issue
Block a user