From 28fff31aec7c4739fcbaed713bf04704b8df0826 Mon Sep 17 00:00:00 2001 From: wtgoku <2687861+wtgoku@users.noreply.github.com> Date: Fri, 29 May 2026 12:02:03 +0800 Subject: [PATCH] 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 ./... --- internal/server/newapi.go | 32 +++++++++++++++- internal/server/video_test.go | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/internal/server/newapi.go b/internal/server/newapi.go index c2f29df..049d8ad 100644 --- a/internal/server/newapi.go +++ b/internal/server/newapi.go @@ -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) { - modelID = strings.TrimSpace(modelID) + modelID = normalizeSeedanceVideoModelID(modelID) if modelID == "" { return nil, errors.New("Seedance model id is required") } @@ -2401,6 +2401,36 @@ func buildSeedanceVideoGenerationPayload(modelID string, input map[string]any) ( 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) { if !c.enabled() { return "", errors.New("PopiNewAPI base URL is not configured") diff --git a/internal/server/video_test.go b/internal/server/video_test.go index 571c6da..1c2dd3b 100644 --- a/internal/server/video_test.go +++ b/internal/server/video_test.go @@ -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) { var gotBody map[string]any 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) { 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" {