Commit 3b465226 by Calcium-Ion Committed by GitHub

feat(ali): support wan3.0 all-in-one video models

* feat(ali): support wan3.0 all-in-one video models

Extend the Alibaba task plugin for wan3.0-video / wan3.0-video-prime
using DashScope media, resolution/ratio, duration rules, and billing ratios.

* fix(ali): make wan3.0 smart duration billable and alias-safe

Review fixes on top of #7240 (qiuliw):

- duration -1 never reached the plugin: the host rejects negative
  canonical duration/seconds facts before any hook runs, on every
  entry point. Decoders now normalize -1 into an auto_duration marker;
  convert emits -1 upstream and bills 30s up front; non-wan3.0 models
  reject the marker instead of silently defaulting to 5s.
- extractUsageOnComplete read output.duration/output.resolution, but
  wan3.0 reports usage.output_video_duration and numeric usage.SR, so
  smart-duration and resolution settlement never reconciled. Read the
  usage block first, keep the legacy output fields as fallback.
- convert keyed default resolution on the client model name, so a
  channel-mapped alias fell to 720P while the direct request got 1080P.
  Every model-shaped decision in convert now uses ctx.upstreamModel.
- Unknown wan3.0 size values were silently coerced to 1080P; reject them.
- Image-only openai_responses input was accepted for every model,
  regressing t2v models into pre-consume then upstream rejection.
  Restore the guard and allow image-only for i2v and wan3.0 only.
- Native passthrough dropped wan3.0 parameters (ratio, audio) unless
  media was present; forward them via metadata for wan3.0.
- Hoist the triplicated size-to-resolution map; add wan3.0 contract tests.

---------

Co-authored-by: qiuliw <a1807191473@qgmail.com>
parent 0c76e4da
package plugins_test
import (
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/pkg/jsplugin"
builtinplugins "github.com/QuantumNous/new-api/plugins"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAlibabaWan3(t *testing.T) {
source, err := builtinplugins.Source("alibaba")
require.NoError(t, err)
registry := jsplugin.NewRegistry()
plugin, err := registry.RegisterFactory(source, jsplugin.Options{Key: "alibaba"})
require.NoError(t, err)
roundTrip := func(t *testing.T, value any) map[string]any {
encoded, marshalErr := common.Marshal(value)
require.NoError(t, marshalErr)
var decoded map[string]any
require.NoError(t, common.Unmarshal(encoded, &decoded))
return decoded
}
submitCtx := func(model, upstream string, body map[string]any) map[string]any {
return map[string]any{"model": model, "upstreamModel": upstream, "baseUrl": "https://dashscope.aliyuncs.com", "apiKey": "k", "requestBody": body}
}
usageCtx := func(purpose string, body map[string]any) map[string]any {
return map[string]any{"model": "wan3.0-video", "upstreamModel": "wan3.0-video", "usagePurpose": purpose, "requestBody": body}
}
decodeResponses := func(model string, body map[string]any) (map[string]any, error) {
value, callErr := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_responses", "decodeRequest"}, map[string]any{"model": model, "body": map[string]any{"kind": "json", "value": body}, "stream": false})
if callErr != nil {
return nil, callErr
}
return roundTrip(t, value), nil
}
t.Run("duration -1 becomes the auto_duration marker so the host accepts the body", func(t *testing.T) {
resolved, callErr := decodeResponses("wan3.0-video", map[string]any{"model": "wan3.0-video", "input": "a cat", "duration": -1})
require.NoError(t, callErr)
requestBody := resolved["requestBody"].(map[string]any)
assert.Equal(t, true, requestBody["auto_duration"])
assert.NotContains(t, requestBody, "duration")
value, callErr := plugin.Engine.CallPath(t.Context(), "protocols", []string{"openai_video", "decodeRequest"}, map[string]any{"model": "wan3.0-video", "body": map[string]any{"kind": "json", "value": map[string]any{"model": "wan3.0-video", "prompt": "a cat", "seconds": -1}}})
require.NoError(t, callErr)
requestBody = roundTrip(t, value)["requestBody"].(map[string]any)
assert.Equal(t, true, requestBody["auto_duration"])
assert.NotContains(t, requestBody, "seconds")
assert.NotContains(t, requestBody, "duration")
value, callErr = plugin.Engine.CallPath(t.Context(), "native", []string{"createVideoTask"}, map[string]any{"body": map[string]any{"kind": "json", "value": map[string]any{
"model": "wan3.0-video", "input": map[string]any{"prompt": "a cat"}, "parameters": map[string]any{"duration": -1, "ratio": "16:9"},
}}})
require.NoError(t, callErr)
requestBody = roundTrip(t, value)["requestBody"].(map[string]any)
assert.Equal(t, true, requestBody["auto_duration"])
assert.NotContains(t, requestBody, "duration")
parameters := requestBody["metadata"].(map[string]any)["parameters"].(map[string]any)
assert.Equal(t, "16:9", parameters["ratio"])
assert.NotContains(t, parameters, "duration")
})
t.Run("auto_duration submits -1 upstream and bills 30 seconds up front", func(t *testing.T) {
body := map[string]any{"model": "wan3.0-video", "prompt": "a cat", "auto_duration": true}
value, callErr := plugin.Engine.Call(t.Context(), "buildSubmitRequest", submitCtx("wan3.0-video", "wan3.0-video", body))
require.NoError(t, callErr)
parameters := roundTrip(t, value)["body"].(map[string]any)["parameters"].(map[string]any)
assert.Equal(t, float64(-1), parameters["duration"])
value, callErr = plugin.Engine.Call(t.Context(), "extractUsage", usageCtx("facts", body))
require.NoError(t, callErr)
assert.Equal(t, map[string]any{"seconds": float64(30), "resolution": "1080P"}, roundTrip(t, value))
value, callErr = plugin.Engine.Call(t.Context(), "extractUsage", usageCtx("billing_ratios", body))
require.NoError(t, callErr)
assert.Equal(t, float64(30), roundTrip(t, value)["seconds"])
_, callErr = plugin.Engine.Call(t.Context(), "buildSubmitRequest", submitCtx("wan2.7-t2v", "wan2.7-t2v", map[string]any{"model": "wan2.7-t2v", "prompt": "a cat", "auto_duration": true}))
require.ErrorContains(t, callErr, "only supported by wan3.0")
})
t.Run("channel-mapped alias resolves defaults from the upstream model", func(t *testing.T) {
direct, callErr := plugin.Engine.Call(t.Context(), "buildSubmitRequest", submitCtx("wan3.0-video", "wan3.0-video", map[string]any{"model": "wan3.0-video", "prompt": "a cat"}))
require.NoError(t, callErr)
alias, callErr := plugin.Engine.Call(t.Context(), "buildSubmitRequest", submitCtx("my-wan3", "wan3.0-video", map[string]any{"model": "my-wan3", "prompt": "a cat"}))
require.NoError(t, callErr)
assert.Equal(t, roundTrip(t, direct)["body"], roundTrip(t, alias)["body"])
assert.Equal(t, "1080P", roundTrip(t, alias)["body"].(map[string]any)["parameters"].(map[string]any)["resolution"])
})
t.Run("size maps to a resolution tier and unknown sizes are rejected", func(t *testing.T) {
value, callErr := plugin.Engine.Call(t.Context(), "buildSubmitRequest", submitCtx("wan3.0-video", "wan3.0-video", map[string]any{"model": "wan3.0-video", "prompt": "a cat", "size": "1280*720"}))
require.NoError(t, callErr)
parameters := roundTrip(t, value)["body"].(map[string]any)["parameters"].(map[string]any)
assert.Equal(t, "720P", parameters["resolution"])
assert.NotContains(t, parameters, "size")
assert.Equal(t, "adaptive", parameters["ratio"])
_, callErr = plugin.Engine.Call(t.Context(), "buildSubmitRequest", submitCtx("wan3.0-video", "wan3.0-video", map[string]any{"model": "wan3.0-video", "prompt": "a cat", "size": "1000*1000"}))
require.ErrorContains(t, callErr, "invalid size")
_, callErr = plugin.Engine.Call(t.Context(), "buildSubmitRequest", submitCtx("wan3.0-video", "wan3.0-video", map[string]any{"model": "wan3.0-video", "prompt": "a cat", "duration": 31}))
require.ErrorContains(t, callErr, "between 2 and 30")
})
t.Run("image-only input stays rejected for t2v models and accepted for wan3.0", func(t *testing.T) {
imageOnly := []any{map[string]any{"type": "input_image", "image_url": "https://cdn.example/first.png"}}
_, callErr := decodeResponses("wan2.7-t2v", map[string]any{"model": "wan2.7-t2v", "input": imageOnly})
require.ErrorContains(t, callErr, "input is required")
resolved, callErr := decodeResponses("wan3.0-video", map[string]any{"model": "wan3.0-video", "input": imageOnly})
require.NoError(t, callErr)
assert.Equal(t, "image_to_video", resolved["action"])
value, callErr := plugin.Engine.Call(t.Context(), "buildSubmitRequest", submitCtx("wan3.0-video", "wan3.0-video", map[string]any{"model": "wan3.0-video", "prompt": "", "images": []any{"https://cdn.example/first.png"}}))
require.NoError(t, callErr)
input := roundTrip(t, value)["body"].(map[string]any)["input"].(map[string]any)
assert.Equal(t, []any{map[string]any{"type": "first_frame", "url": "https://cdn.example/first.png"}}, input["media"])
assert.NotContains(t, input, "img_url")
})
t.Run("completion facts read the wan3.0 usage block", func(t *testing.T) {
value, callErr := plugin.Engine.Call(t.Context(), "extractUsageOnComplete", map[string]any{}, map[string]any{}, map[string]any{
"output": map[string]any{"task_status": "SUCCEEDED", "video_url": "https://upstream.example/v.mp4"},
"usage": map[string]any{"video_count": 1, "duration": 7.5, "output_video_duration": 7.5, "SR": 720, "ratio": "16:9"},
})
require.NoError(t, callErr)
assert.Equal(t, map[string]any{"seconds": 7.5, "resolution": "720P"}, roundTrip(t, value))
value, callErr = plugin.Engine.Call(t.Context(), "extractUsageOnComplete", map[string]any{}, map[string]any{}, map[string]any{
"output": map[string]any{"task_status": "SUCCEEDED", "duration": 5, "resolution": "1080p"},
})
require.NoError(t, callErr)
assert.Equal(t, map[string]any{"seconds": float64(5), "resolution": "1080P"}, roundTrip(t, value))
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment