feat(seedance): 两个已开通模型的限制表与提交前本地预校验(23 测试)
账号实际接入 doubao-seedance-2-5-260628 与 doubao-seedance-2-0-260128,两者限制 差异不小,且同一组参数在一个上合法、在另一个上不合法: | | 2.5 | 2.0 | | 时长 | [4, 30] | [4, 15] | | 分辨率 | 480p/720p/1080p | 480p/720p/1080p/4k | | 图生视频 ratio | **只能 adaptive** | 可指定 | | output_format | mp4/mov | 仅 mp4 | 超范围的请求会被厂商 400 拒绝——那要花一次往返、计入 RPM,并表现为一个失败的 任务。故在 capabilities.py 落一张限制表并在提交前本地预校验,错误信息直接指出 是哪个模型的哪条限制。 两处刻意的设计: - **按模型族前缀匹配**而非完整 ID:尾部是日期版本(260628/260128),会随厂商 小版本更新而变,按前缀匹配使一次升级不必改表。 - **未知模型一律放行**(只记日志):账号新开通一个模型时不该因为这张表没更新 就用不了,否则「开通即可用」会退化成「开通还得改代码发版」。代价只是失去 本地预校验,厂商仍会兜底。 测试 100 → 123:限制表 23 项逐条对应官方文档的一条限制,另在适配器层加 4 项 端到端断言(超范围请求的 handler 直接 assert 不该被调用,确认请求根本没发出)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""模型限制表的测试。
|
||||
|
||||
每个用例对应官方文档里两个已接入模型的一条具体限制。这些限制若搞错,表现是
|
||||
用户提交后拿到一个失败任务(厂商 400),而本地测试全绿——所以值得逐条钉住。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.providers.volcengine.capabilities import (
|
||||
AUTO_DURATION,
|
||||
SEEDANCE_2_0,
|
||||
SEEDANCE_2_5,
|
||||
limits_for,
|
||||
validate,
|
||||
)
|
||||
|
||||
#: 账号实际接入的两个模型 ID。
|
||||
MODEL_2_5 = "doubao-seedance-2-5-260628"
|
||||
MODEL_2_0 = "doubao-seedance-2-0-260128"
|
||||
|
||||
|
||||
class TestFamilyDetection:
|
||||
def test_both_activated_models_are_recognised(self):
|
||||
assert limits_for(MODEL_2_5) is SEEDANCE_2_5
|
||||
assert limits_for(MODEL_2_0) is SEEDANCE_2_0
|
||||
|
||||
def test_matching_is_by_family_prefix_not_exact_id(self):
|
||||
"""尾部日期版本会随厂商更新而变。
|
||||
|
||||
按前缀匹配使一次模型小版本升级(如 2-5-261231)不需要改这张表。
|
||||
"""
|
||||
assert limits_for("doubao-seedance-2-5-999999") is SEEDANCE_2_5
|
||||
|
||||
def test_unknown_model_has_no_limits(self):
|
||||
assert limits_for("some-future-model") is None
|
||||
|
||||
|
||||
class TestUnknownModelsArePassedThrough:
|
||||
"""账号新开通一个模型时不该因为这张表没更新就用不了。
|
||||
|
||||
否则「开通即可用」会变成「开通还得改代码发版」。代价只是失去本地预校验。
|
||||
"""
|
||||
|
||||
def test_anything_goes_for_an_unknown_model(self):
|
||||
validate(
|
||||
"brand-new-model",
|
||||
resolution="8k",
|
||||
duration_sec=999,
|
||||
ratio="16:9",
|
||||
has_first_frame=True,
|
||||
)
|
||||
|
||||
|
||||
class TestResolutionLimits:
|
||||
def test_2_5_has_no_4k(self):
|
||||
# 官方:2.5 可选 480p / 720p / 1080p。
|
||||
with pytest.raises(ValueError, match="不支持分辨率 4k"):
|
||||
validate(MODEL_2_5, resolution="4k")
|
||||
|
||||
def test_2_0_does_have_4k(self):
|
||||
validate(MODEL_2_0, resolution="4k")
|
||||
|
||||
def test_shared_resolutions_work_on_both(self):
|
||||
for model in (MODEL_2_5, MODEL_2_0):
|
||||
for resolution in ("480p", "720p", "1080p"):
|
||||
validate(model, resolution=resolution)
|
||||
|
||||
def test_error_names_the_model_and_lists_the_options(self):
|
||||
with pytest.raises(ValueError) as exc:
|
||||
validate(MODEL_2_5, resolution="4k")
|
||||
assert MODEL_2_5 in str(exc.value)
|
||||
assert "1080p" in str(exc.value)
|
||||
|
||||
|
||||
class TestDurationLimits:
|
||||
def test_2_5_allows_up_to_30_seconds(self):
|
||||
validate(MODEL_2_5, duration_sec=30)
|
||||
|
||||
def test_2_0_caps_at_15_seconds(self):
|
||||
# 官方:2.0 系列取值范围 [4, 15]。
|
||||
validate(MODEL_2_0, duration_sec=15)
|
||||
with pytest.raises(ValueError, match=r"\[4, 15\]"):
|
||||
validate(MODEL_2_0, duration_sec=20)
|
||||
|
||||
def test_the_same_20_seconds_is_fine_on_2_5(self):
|
||||
"""同一个时长在两个模型上一个合法一个不合法——这正是需要这张表的理由。"""
|
||||
validate(MODEL_2_5, duration_sec=20)
|
||||
with pytest.raises(ValueError):
|
||||
validate(MODEL_2_0, duration_sec=20)
|
||||
|
||||
def test_below_minimum_is_rejected_on_both(self):
|
||||
for model in (MODEL_2_5, MODEL_2_0):
|
||||
with pytest.raises(ValueError):
|
||||
validate(model, duration_sec=2)
|
||||
|
||||
def test_auto_duration_bypasses_the_range_check(self):
|
||||
# -1 表示交给模型在有效范围内自选,不该被范围校验拦下。
|
||||
validate(MODEL_2_5, duration_sec=AUTO_DURATION)
|
||||
validate(MODEL_2_0, duration_sec=AUTO_DURATION)
|
||||
|
||||
def test_omitted_duration_is_not_checked(self):
|
||||
validate(MODEL_2_5)
|
||||
|
||||
|
||||
class TestRatioLockedToAdaptiveOnImageToVideo:
|
||||
"""2.5 在图生视频时输出宽高比自动与首帧图片一致,不接受指定值。"""
|
||||
|
||||
def test_2_5_rejects_a_specific_ratio_when_a_first_frame_is_given(self):
|
||||
with pytest.raises(ValueError, match="只支持 ratio=adaptive"):
|
||||
validate(MODEL_2_5, ratio="16:9", has_first_frame=True)
|
||||
|
||||
def test_2_5_accepts_adaptive_with_a_first_frame(self):
|
||||
validate(MODEL_2_5, ratio="adaptive", has_first_frame=True)
|
||||
|
||||
def test_2_5_accepts_a_specific_ratio_for_text_to_video(self):
|
||||
# 限制只在图生视频场景成立;文生视频可指定宽高比。
|
||||
validate(MODEL_2_5, ratio="16:9", has_first_frame=False)
|
||||
|
||||
def test_2_0_accepts_a_specific_ratio_even_with_a_first_frame(self):
|
||||
validate(MODEL_2_0, ratio="16:9", has_first_frame=True)
|
||||
|
||||
|
||||
class TestOutputFormatSupport:
|
||||
def test_only_2_5_supports_mov(self):
|
||||
# 我们当前不发送 output_format(默认 mp4),但这张表记录了差异,
|
||||
# 以便将来开放该参数时不必重读文档。
|
||||
assert SEEDANCE_2_5.supports_output_format is True
|
||||
assert SEEDANCE_2_0.supports_output_format is False
|
||||
Reference in New Issue
Block a user