Route friendly Seedance aliases to configured upstream models
Popiai can emit human-facing model names such as seedance2.0, while the upstream channel table is keyed by the canonical doubao-seedance model ids. Normalize Seedance aliases at the server gateway boundary so CLI, Popiai, and any direct gateway-compatible client can keep using friendly names without sending an unconfigured model id upstream. Constraint: NewAPI channel selection requires canonical doubao-seedance-* model ids. Rejected: Fix only the PopiArt CLI default path | Popiai and other clients can still send explicit friendly aliases. Confidence: high Scope-risk: narrow Directive: Keep alias normalization at the server boundary before expanding client-specific mappings. Tested: go test ./internal/server -run 'Seedance|VideoGenerations' Tested: go test ./...
This commit is contained in:
@@ -2339,7 +2339,7 @@ func (c *newAPIClient) submitSeedanceVideoTask(ctx context.Context, token, model
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildSeedanceVideoGenerationPayload(modelID string, input map[string]any) (map[string]any, error) {
|
func buildSeedanceVideoGenerationPayload(modelID string, input map[string]any) (map[string]any, error) {
|
||||||
modelID = strings.TrimSpace(modelID)
|
modelID = normalizeSeedanceVideoModelID(modelID)
|
||||||
if modelID == "" {
|
if modelID == "" {
|
||||||
return nil, errors.New("Seedance model id is required")
|
return nil, errors.New("Seedance model id is required")
|
||||||
}
|
}
|
||||||
@@ -2401,6 +2401,36 @@ func buildSeedanceVideoGenerationPayload(modelID string, input map[string]any) (
|
|||||||
return payload, nil
|
return payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeSeedanceVideoModelID(modelID string) string {
|
||||||
|
modelID = strings.TrimSpace(modelID)
|
||||||
|
if modelID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized := strings.ToLower(strings.NewReplacer("_", "-", ".", "-").Replace(modelID))
|
||||||
|
normalized = strings.Join(strings.Fields(normalized), "")
|
||||||
|
|
||||||
|
switch normalized {
|
||||||
|
case "seedance2-0", "seedance-2-0":
|
||||||
|
return "doubao-seedance-2-0-260128"
|
||||||
|
case "seedance2-0-fast", "seedance-2-0-fast":
|
||||||
|
return "doubao-seedance-2-0-fast-260128"
|
||||||
|
case "seedance1-5", "seedance-1-5", "seedance1-5-pro", "seedance-1-5-pro":
|
||||||
|
return "doubao-seedance-1-5-pro-251215"
|
||||||
|
case "seedance1-0-pro", "seedance-1-0-pro":
|
||||||
|
return "doubao-seedance-1-0-pro-250528"
|
||||||
|
case "seedance1-0-lite-t2v", "seedance-1-0-lite-t2v":
|
||||||
|
return "doubao-seedance-1-0-lite-t2v"
|
||||||
|
case "seedance1-0-lite-i2v", "seedance-1-0-lite-i2v":
|
||||||
|
return "doubao-seedance-1-0-lite-i2v"
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(normalized, "seedance-") {
|
||||||
|
return "doubao-" + normalized
|
||||||
|
}
|
||||||
|
return modelID
|
||||||
|
}
|
||||||
|
|
||||||
func (c *newAPIClient) submitJimengVideoTask(ctx context.Context, token, modelID string, input map[string]any) (string, error) {
|
func (c *newAPIClient) submitJimengVideoTask(ctx context.Context, token, modelID string, input map[string]any) (string, error) {
|
||||||
if !c.enabled() {
|
if !c.enabled() {
|
||||||
return "", errors.New("PopiNewAPI base URL is not configured")
|
return "", errors.New("PopiNewAPI base URL is not configured")
|
||||||
|
|||||||
@@ -405,6 +405,33 @@ func TestSubmitSeedanceVideoTaskUsesVideoGenerationsEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSubmitSeedanceVideoTaskNormalizesFriendlyModelAlias(t *testing.T) {
|
||||||
|
var gotBody map[string]any
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||||
|
t.Fatalf("decode request body: %v", err)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"id":"task_seedance_alias_123","status":"submitted"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client := newNewAPIClient(Config{NewAPIBaseURL: srv.URL})
|
||||||
|
taskID, err := client.submitSeedanceVideoTask(context.Background(), "sk-test", "seedance2.0", map[string]any{
|
||||||
|
"prompt": "keep the motion natural",
|
||||||
|
"duration": 5,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("submitSeedanceVideoTask: %v", err)
|
||||||
|
}
|
||||||
|
if taskID != "task_seedance_alias_123" {
|
||||||
|
t.Fatalf("unexpected task id: %q", taskID)
|
||||||
|
}
|
||||||
|
if gotBody["model"] != "doubao-seedance-2-0-260128" {
|
||||||
|
t.Fatalf("expected canonical Seedance model, got %#v", gotBody["model"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleVideoGenerationsRelaysSeedanceRequest(t *testing.T) {
|
func TestHandleVideoGenerationsRelaysSeedanceRequest(t *testing.T) {
|
||||||
var gotBody map[string]any
|
var gotBody map[string]any
|
||||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -466,6 +493,48 @@ func TestHandleVideoGenerationsRelaysSeedanceRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleVideoGenerationsNormalizesSeedanceAlias(t *testing.T) {
|
||||||
|
var gotBody map[string]any
|
||||||
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||||
|
t.Fatalf("decode upstream body: %v", err)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"code":"success","data":{"task_id":"task_seedance_alias_direct_123","status":"PENDING"}}`))
|
||||||
|
}))
|
||||||
|
defer upstream.Close()
|
||||||
|
|
||||||
|
server, err := NewWithConfig(Config{
|
||||||
|
NewAPIBaseURL: upstream.URL,
|
||||||
|
SQLitePath: filepath.Join(t.TempDir(), "popiart.db"),
|
||||||
|
SkillhubDir: makeEmptySkillhub(t),
|
||||||
|
SessionSecret: "test-secret",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewWithConfig: %v", err)
|
||||||
|
}
|
||||||
|
token, _, ok, err := server.store.createSession("sk-test-upstream")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createSession: %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected session creation to succeed")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/video/generations", strings.NewReader(`{"model":"seedance2.0","prompt":"ping"}`))
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusAccepted {
|
||||||
|
t.Fatalf("expected 202, got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if gotBody["model"] != "doubao-seedance-2-0-260128" {
|
||||||
|
t.Fatalf("expected canonical Seedance model, got %#v", gotBody["model"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleVideoGenerationFetchSurfacesLastFrameURL(t *testing.T) {
|
func TestHandleVideoGenerationFetchSurfacesLastFrameURL(t *testing.T) {
|
||||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet || r.URL.Path != "/v1/video/generations/task_seedance_direct_123" {
|
if r.Method != http.MethodGet || r.URL.Path != "/v1/video/generations/task_seedance_direct_123" {
|
||||||
|
|||||||
Reference in New Issue
Block a user