Browse Source

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 ./...
master
wtgoku 2 months ago
parent
commit
28fff31aec
  1. 32
      internal/server/newapi.go
  2. 69
      internal/server/video_test.go

32
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) { 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")

69
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) { 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" {

Loading…
Cancel
Save