fix(video): runway/higgsfield model defaults diverge from schema

runway (estimate_cost/estimate_runtime/execute) defaulted to gen4_turbo and
higgsfield (execute) defaulted to kling_3.0, while both schemas advertise
seedance_2.0 as model.default. Omitting model under-quoted cost (runway 6x:
$0.25 vs $1.50) and silently generated a different model than advertised,
violating the Decision-Communication / cost-accuracy contract.

Root cause was a default duplicated across schema + 3 methods that drifted.
Collapse it to a single _DEFAULT_MODEL constant referenced everywhere.

Add tests/tools/test_provider_model_defaults.py to lock each tool's
estimate default to its schema default and guard the execute path.
This commit is contained in:
An-idd
2026-06-26 15:30:22 +08:00
parent 0d917ea2ca
commit 93d96de8f1
3 changed files with 62 additions and 8 deletions

View File

@@ -0,0 +1,44 @@
"""Regression: a provider's code-level model default must match its schema's
declared `model.default`.
Bug: runway_video and higgsfield_video hardcoded stale model defaults in
estimate_cost/estimate_runtime/execute (`gen4_turbo` / `kling_3.0`) while the
schema advertised `seedance_2.0` as the premium default. Omitting `model` then
quoted the wrong (cheap) model's cost and silently generated a different model
than the schema promised — a Decision-Communication / cost-accuracy violation.
"""
import pytest
import tools.video.higgsfield_video as higgsfield_video
import tools.video.runway_video as runway_video
from tools.video.higgsfield_video import HiggsFieldVideo
from tools.video.runway_video import RunwayVideo
@pytest.mark.parametrize(
"tool_cls, module",
[(RunwayVideo, runway_video), (HiggsFieldVideo, higgsfield_video)],
)
def test_default_model_constant_matches_schema(tool_cls, module):
# `execute()` reads `model` via `_DEFAULT_MODEL`; locking the constant to the
# schema default guards the silent-model-swap path without a network call.
schema_default = tool_cls().input_schema["properties"]["model"]["default"]
assert module._DEFAULT_MODEL == schema_default
@pytest.mark.parametrize("tool_cls", [RunwayVideo, HiggsFieldVideo])
def test_estimate_default_model_matches_schema(tool_cls):
tool = tool_cls()
schema_default = tool.input_schema["properties"]["model"]["default"]
# Cost/runtime with `model` omitted must equal the schema's declared default,
# not some stale hardcoded fallback.
assert tool.estimate_cost({}) == tool.estimate_cost({"model": schema_default}), (
f"{tool.name}.estimate_cost default model diverges from schema default "
f"{schema_default!r}"
)
assert tool.estimate_runtime({}) == tool.estimate_runtime({"model": schema_default}), (
f"{tool.name}.estimate_runtime default model diverges from schema default "
f"{schema_default!r}"
)

View File

@@ -24,6 +24,11 @@ from tools.base_tool import (
ToolTier,
)
# Single source of truth for the default model. Referenced by both the input
# schema and every code path that reads `model`, so estimate_cost / estimate_runtime
# / execute can never silently diverge from the advertised default again.
_DEFAULT_MODEL = "seedance_2.0"
class HiggsFieldVideo(BaseTool):
name = "higgsfield_video"
@@ -89,7 +94,7 @@ class HiggsFieldVideo(BaseTool):
"wan_2.5",
"soul_cinema",
],
"default": "seedance_2.0",
"default": _DEFAULT_MODEL,
"description": "Underlying model. Defaults to Seedance 2.0 (preferred premium) — see .agents/skills/seedance-2-0/",
},
"duration": {
@@ -134,7 +139,7 @@ class HiggsFieldVideo(BaseTool):
return ToolStatus.UNAVAILABLE
def estimate_cost(self, inputs: dict[str, Any]) -> float:
model = inputs.get("model", "seedance_2.0")
model = inputs.get("model", _DEFAULT_MODEL)
duration = int(inputs.get("duration", "5"))
# Approximate per-clip costs based on Higgsfield credit pricing.
# Seedance 2.0 on Higgsfield runs ~50-80 credits per 5s clip ≈ $0.50-$1.20.
@@ -151,7 +156,7 @@ class HiggsFieldVideo(BaseTool):
return base * (duration / 5)
def estimate_runtime(self, inputs: dict[str, Any]) -> float:
model = inputs.get("model", "seedance_2.0")
model = inputs.get("model", _DEFAULT_MODEL)
if model in ("veo_3.1", "sora_2", "seedance_2.0"):
return 120.0
if model == "seedance_2.0_fast":
@@ -171,7 +176,7 @@ class HiggsFieldVideo(BaseTool):
api_key, api_secret = creds
start = time.time()
operation = inputs.get("operation", "text_to_video")
model = inputs.get("model", "kling_3.0")
model = inputs.get("model", _DEFAULT_MODEL)
payload: dict[str, Any] = {
"prompt": inputs["prompt"],

View File

@@ -47,6 +47,11 @@ _RUNTIME_SECONDS = {
"seedance_2.0_fast": 60.0,
}
# Single source of truth for the default model. Referenced by both the input
# schema and every code path that reads `model`, so estimate_cost / estimate_runtime
# / execute can never silently diverge from the advertised default again.
_DEFAULT_MODEL = "seedance_2.0"
class RunwayVideo(BaseTool):
name = "runway_video"
@@ -101,7 +106,7 @@ class RunwayVideo(BaseTool):
"model": {
"type": "string",
"enum": ["seedance_2.0", "seedance_2.0_fast", "gen4_turbo", "gen4_aleph", "gen3a_turbo"],
"default": "seedance_2.0",
"default": _DEFAULT_MODEL,
"description": (
"seedance_2.0 = preferred premium default (single-pass synced audio, multi-shot, lip-sync — "
"Runway Unlimited/Enterprise plan, non-US only). "
@@ -149,12 +154,12 @@ class RunwayVideo(BaseTool):
return os.environ.get("RUNWAY_API_KEY") or os.environ.get("RUNWAYML_API_SECRET")
def estimate_cost(self, inputs: dict[str, Any]) -> float:
model = inputs.get("model", "gen4_turbo")
model = inputs.get("model", _DEFAULT_MODEL)
duration = inputs.get("duration", 5)
return _COST_PER_SECOND.get(model, 0.05) * duration
def estimate_runtime(self, inputs: dict[str, Any]) -> float:
model = inputs.get("model", "gen4_turbo")
model = inputs.get("model", _DEFAULT_MODEL)
return _RUNTIME_SECONDS.get(model, 30.0)
def execute(self, inputs: dict[str, Any]) -> ToolResult:
@@ -168,7 +173,7 @@ class RunwayVideo(BaseTool):
import requests
start = time.time()
model = inputs.get("model", "gen4_turbo")
model = inputs.get("model", _DEFAULT_MODEL)
operation = inputs.get("operation", "text_to_video")
ratio_friendly = inputs.get("ratio", "16:9")
ratio_pixels = _RATIO_MAP.get(ratio_friendly, "1280:720")