增加模型别名

This commit is contained in:
2026-07-04 14:49:52 +08:00
parent 641a021f7a
commit 03f9744dcb
16 changed files with 298 additions and 145 deletions
+23 -18
View File
@@ -74,6 +74,11 @@ func (h *AdminReadHandler) Logs(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load logs"})
return
}
modelByID, err := h.admin.ModelNameMap(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load logs"})
return
}
out := make([]gin.H, 0, len(items))
for _, item := range items {
var userName any
@@ -97,7 +102,7 @@ func (h *AdminReadHandler) Logs(c *gin.Context) {
"ts": item.TS.Unix(),
"kind": item.Kind,
"status": item.Status,
"model": item.Model,
"model": displayModelName(modelByID, item.Model),
"provider": item.Provider,
"prompt": item.Prompt,
"ratio": item.Ratio,
@@ -192,24 +197,24 @@ func userPublic(user model.User) gin.H {
})
}
return gin.H{
"id": user.ID,
"email": user.Email,
"name": user.Name,
"role": user.Role,
"status": user.Status,
"credits": user.Credits,
"notes": user.Notes,
"recharge_total": user.RechargeTotal,
"id": user.ID,
"email": user.Email,
"name": user.Name,
"role": user.Role,
"status": user.Status,
"credits": user.Credits,
"notes": user.Notes,
"recharge_total": user.RechargeTotal,
"concurrency_group_id": user.ConcurrencyGroupID,
"created_at": unixSec(user.CreatedAt),
"last_login_at": unixSecPtr(user.LastLoginAt),
"last_login_ip": user.LastLoginIP,
"invite_code": user.InviteCode,
"invited_by": user.InvitedBy,
"checkin_last": user.CheckinLast,
"checkin_streak": user.CheckinStreak,
"api_keys": keys,
"has_password": user.PasswordHash != "",
"created_at": unixSec(user.CreatedAt),
"last_login_at": unixSecPtr(user.LastLoginAt),
"last_login_ip": user.LastLoginIP,
"invite_code": user.InviteCode,
"invited_by": user.InvitedBy,
"checkin_last": user.CheckinLast,
"checkin_streak": user.CheckinStreak,
"api_keys": keys,
"has_password": user.PasswordHash != "",
}
}
@@ -190,6 +190,10 @@ func (h *AdminWriteHandler) CreateModel(c *gin.Context) {
}
item, err := h.admin.CreateModel(c.Request.Context(), body)
if err != nil {
if errors.Is(err, service.ErrModelAliasCollision) {
c.JSON(http.StatusConflict, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
@@ -204,6 +208,10 @@ func (h *AdminWriteHandler) UpdateModel(c *gin.Context) {
}
item, err := h.admin.UpdateModel(c.Request.Context(), c.Param("model_id"), body)
if err != nil {
if errors.Is(err, service.ErrModelAliasCollision) {
c.JSON(http.StatusConflict, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
@@ -0,0 +1,16 @@
package handler
import "strings"
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
}
@@ -225,6 +225,11 @@ func (h *UserGenerationHandler) Logs(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load logs"})
return
}
modelByID, err := h.admin.ModelNameMap(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load logs"})
return
}
out := make([]gin.H, 0, len(items))
for _, item := range items {
@@ -249,7 +254,7 @@ func (h *UserGenerationHandler) Logs(c *gin.Context) {
"ts": item.TS.Unix(),
"kind": item.Kind,
"status": item.Status,
"model": item.Model,
"model": displayModelName(modelByID, item.Model),
"provider": item.Provider,
"prompt": item.Prompt,
"ratio": item.Ratio,
+82 -73
View File
@@ -1,35 +1,36 @@
package model
import (
"strings"
"time"
"gorm.io/datatypes"
)
type User struct {
ID string `gorm:"primaryKey;size:32"`
Email string `gorm:"size:255;uniqueIndex;not null"`
Name string `gorm:"size:255"`
PasswordHash string `gorm:"size:255"`
Role string `gorm:"size:32;index;not null"`
Status string `gorm:"size:32;index;not null"`
Credits float64 `gorm:"not null;default:0"`
Notes string `gorm:"type:text"`
ConcurrencyGroupID string `gorm:"size:32;index"`
AnnouncementSeen string `gorm:"size:32"` // version hash of the last announcement this user dismissed
ID string `gorm:"primaryKey;size:32"`
Email string `gorm:"size:255;uniqueIndex;not null"`
Name string `gorm:"size:255"`
PasswordHash string `gorm:"size:255"`
Role string `gorm:"size:32;index;not null"`
Status string `gorm:"size:32;index;not null"`
Credits float64 `gorm:"not null;default:0"`
Notes string `gorm:"type:text"`
ConcurrencyGroupID string `gorm:"size:32;index"`
AnnouncementSeen string `gorm:"size:32"` // version hash of the last announcement this user dismissed
RechargeTotal float64 `gorm:"not null;default:0"` // 累计充值金额(元)
InviteCode string `gorm:"size:32;uniqueIndex"`
InvitedBy *string `gorm:"size:32;index"`
InviteRewardDone bool `gorm:"not null;default:false"`
InviteRewardAt *time.Time
CheckinLast string `gorm:"size:32"`
CheckinStreak int `gorm:"not null;default:0"`
GenerationCount int64 `gorm:"not null;default:0"`
LastLoginAt *time.Time
LastLoginIP string `gorm:"size:128"`
CreatedAt time.Time
UpdatedAt time.Time
APIKeys []APIKey `gorm:"foreignKey:UserID"`
InviteCode string `gorm:"size:32;uniqueIndex"`
InvitedBy *string `gorm:"size:32;index"`
InviteRewardDone bool `gorm:"not null;default:false"`
InviteRewardAt *time.Time
CheckinLast string `gorm:"size:32"`
CheckinStreak int `gorm:"not null;default:0"`
GenerationCount int64 `gorm:"not null;default:0"`
LastLoginAt *time.Time
LastLoginIP string `gorm:"size:128"`
CreatedAt time.Time
UpdatedAt time.Time
APIKeys []APIKey `gorm:"foreignKey:UserID"`
}
type APIKey struct {
@@ -57,16 +58,16 @@ type ShowcaseItem struct {
}
type EventLog struct {
ID string `gorm:"primaryKey;size:32"`
TS time.Time `gorm:"index;not null"`
Kind string `gorm:"size:32;index;not null"`
Status string `gorm:"size:32;index;not null"`
Model string `gorm:"size:255;index"`
Provider string `gorm:"size:100;index"`
Prompt string `gorm:"type:text"`
Ratio string `gorm:"size:32"`
Resolution string `gorm:"size:32"`
Duration string `gorm:"size:32"`
ID string `gorm:"primaryKey;size:32"`
TS time.Time `gorm:"index;not null"`
Kind string `gorm:"size:32;index;not null"`
Status string `gorm:"size:32;index;not null"`
Model string `gorm:"size:255;index"`
Provider string `gorm:"size:100;index"`
Prompt string `gorm:"type:text"`
Ratio string `gorm:"size:32"`
Resolution string `gorm:"size:32"`
Duration string `gorm:"size:32"`
Refs int `gorm:"not null;default:0"`
RefFiles datatypes.JSON `gorm:"type:jsonb"` // relative paths of saved reference images, for回显 on reload
Source string `gorm:"size:32;index"`
@@ -74,39 +75,40 @@ type EventLog struct {
// stamped when the upstream call begins. Drives the accounts view's live
// in-flight count (pending events per account) and lets an abandoned-event
// purge attribute the failure back to the account it was using.
AccountID string `gorm:"size:64;index"`
UserID string `gorm:"size:32;index"`
Cost float64 `gorm:"not null;default:0"`
AccountID string `gorm:"size:64;index"`
UserID string `gorm:"size:32;index"`
Cost float64 `gorm:"not null;default:0"`
// Refunded marks that this event's up-front charge has already been credited
// back, so the normal failure path and the abandoned-purge sweep can never
// double-refund the same generation.
Refunded bool `gorm:"not null;default:false"`
ElapsedMS int `gorm:"not null;default:0"`
File string `gorm:"size:500;index"`
Error string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
Refunded bool `gorm:"not null;default:false"`
ElapsedMS int `gorm:"not null;default:0"`
File string `gorm:"size:500;index"`
Error string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
}
type ModelConfig struct {
ID string `gorm:"primaryKey;size:255"`
Type string `gorm:"size:32;index;not null"`
Name string `gorm:"size:255;not null"`
Provider string `gorm:"size:100;index;not null"`
Enabled bool `gorm:"not null;default:true"`
Ratios datatypes.JSON `gorm:"type:jsonb"`
Prices datatypes.JSONMap `gorm:"type:jsonb"`
Resolutions datatypes.JSON `gorm:"type:jsonb"`
ImageToImage bool `gorm:"not null;default:false"`
DurationPrices datatypes.JSONMap `gorm:"type:jsonb"`
ID string `gorm:"primaryKey;size:255"`
Type string `gorm:"size:32;index;not null"`
Name string `gorm:"size:255;not null"`
Alias string `gorm:"column:alias;size:255;not null;default:''"`
Provider string `gorm:"size:100;index;not null"`
Enabled bool `gorm:"not null;default:true"`
Ratios datatypes.JSON `gorm:"type:jsonb"`
Prices datatypes.JSONMap `gorm:"type:jsonb"`
Resolutions datatypes.JSON `gorm:"type:jsonb"`
ImageToImage bool `gorm:"not null;default:false"`
DurationPrices datatypes.JSONMap `gorm:"type:jsonb"`
// Agent (代理) pricing — optional overlay over Prices/DurationPrices. A tier
// left unset here means agent users pay the normal price for that tier; the
// set of *supported* tiers is always driven by Prices, not these.
PricesAgent datatypes.JSONMap `gorm:"type:jsonb;column:prices_agent"`
DurationPricesAgent datatypes.JSONMap `gorm:"type:jsonb;column:duration_prices_agent"`
Durations datatypes.JSON `gorm:"type:jsonb"`
MaxReferenceImages int `gorm:"not null;default:0"`
ReferenceMode string `gorm:"size:32;not null;default:'none'"`
Durations datatypes.JSON `gorm:"type:jsonb"`
MaxReferenceImages int `gorm:"not null;default:0"`
ReferenceMode string `gorm:"size:32;not null;default:'none'"`
// Custom-upstream models (provider="custom"): UpstreamModel is the model name
// sent to the upstream OpenAI-compatible API; the base_url + key live on the
// matching custom account (pool="custom", meta.base_url). Empty for built-ins.
@@ -114,13 +116,20 @@ type ModelConfig struct {
// Weight controls display order in the model dropdown / admin list: higher
// weight floats to the top (matches ShowcaseItem.Weight semantics). Ties fall
// back to created_at desc. Default 0.
Weight int `gorm:"not null;default:0;index"`
Weight int `gorm:"not null;default:0;index"`
// GenerationCount is a persistent success counter, incremented once per
// successful generation. Independent of the event_log (which is subject to
// retention / manual clearing), so the admin "次数" is a true running total.
GenerationCount int64 `gorm:"not null;default:0"`
CreatedAt time.Time
UpdatedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
func (m ModelConfig) EffectiveName() string {
if strings.TrimSpace(m.Alias) != "" {
return strings.TrimSpace(m.Alias)
}
return m.ID
}
type CDKCode struct {
@@ -154,19 +163,19 @@ type TokenAccount struct {
// enters the shared "quota" waiting status when BOTH are limited; a single
// limit leaves the account usable for the other kind. Recovery time is shared
// (QuotaRecoverAt / CachedQuotaResetAfter) since Adobe resets both at once.
ImageLimited bool `gorm:"not null;default:false"`
VideoLimited bool `gorm:"not null;default:false"`
AccountEmail string `gorm:"size:255"`
AccountDisplayName string `gorm:"size:255"`
ImageLimited bool `gorm:"not null;default:false"`
VideoLimited bool `gorm:"not null;default:false"`
AccountEmail string `gorm:"size:255"`
AccountDisplayName string `gorm:"size:255"`
// Weight biases scheduling order for ANY account — higher weight is picked
// first within its pool (ties fall back to round-robin). Default 0.
Weight int `gorm:"not null;default:0"`
// Concurrency is the max simultaneous jobs for THIS account. Only custom
// (upstream) accounts honor it; built-in pools use their system default
// (1 per account, grok 10). 0 = use the system default.
Concurrency int `gorm:"not null;default:0"`
CreatedAt time.Time
UpdatedAt time.Time
Concurrency int `gorm:"not null;default:0"`
CreatedAt time.Time
UpdatedAt time.Time
}
type RefreshProfile struct {
@@ -215,15 +224,15 @@ func AutoMigrateModels() []any {
// order number (out_trade_no). Status: pending | paid | cancelled. Unpaid orders
// auto-cancel 30 min after creation (ExpiresAt).
type Order struct {
ID string `gorm:"primaryKey;size:40"`
UserID string `gorm:"size:32;index;not null"`
Amount float64 `gorm:"not null"` // 充值金额(元)
Points int `gorm:"not null"` // 到账积分
PayType string `gorm:"size:16"` // wxpay | alipay
Status string `gorm:"size:16;index;not null"` // pending | paid | cancelled
TradeNo string `gorm:"size:64;index"` // 易支付平台订单号
PayInfo string `gorm:"type:text"` // 二维码 url / 跳转 url
PayInfoType string `gorm:"size:16"` // qrcode | jump | html | ...
ID string `gorm:"primaryKey;size:40"`
UserID string `gorm:"size:32;index;not null"`
Amount float64 `gorm:"not null"` // 充值金额(元)
Points int `gorm:"not null"` // 到账积分
PayType string `gorm:"size:16"` // wxpay | alipay
Status string `gorm:"size:16;index;not null"` // pending | paid | cancelled
TradeNo string `gorm:"size:64;index"` // 易支付平台订单号
PayInfo string `gorm:"type:text"` // 二维码 url / 跳转 url
PayInfoType string `gorm:"size:16"` // qrcode | jump | html | ...
ExpiresAt time.Time `gorm:"index"`
PaidAt *time.Time
CreatedAt time.Time
+13 -1
View File
@@ -38,12 +38,24 @@ func (r *ModelRepository) List(ctx context.Context) ([]model.ModelConfig, error)
func (r *ModelRepository) Get(ctx context.Context, modelID string) (*model.ModelConfig, error) {
var item model.ModelConfig
if err := r.db.WithContext(ctx).First(&item, "id = ?", modelID).Error; err != nil {
if err := r.db.WithContext(ctx).First(&item, "(alias <> '' AND alias = ?) OR (alias = '' AND id = ?)", modelID, modelID).Error; err != nil {
return nil, err
}
return &item, nil
}
func (r *ModelRepository) NameMap(ctx context.Context) (map[string]string, error) {
items, err := r.List(ctx)
if err != nil {
return nil, err
}
out := make(map[string]string, len(items))
for _, item := range items {
out[item.ID] = item.EffectiveName()
}
return out, nil
}
func JSONStrings(v datatypes.JSON) []string {
if len(v) == 0 {
return []string{}
+5
View File
@@ -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,
+71 -27
View File
@@ -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 {
+25 -4
View File
@@ -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 {
+13 -7
View File
@@ -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