diff --git a/tests/tools/test_provider_model_defaults.py b/tests/tools/test_provider_model_defaults.py new file mode 100644 index 00000000..cc2ef689 --- /dev/null +++ b/tests/tools/test_provider_model_defaults.py @@ -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}" + ) diff --git a/tools/video/higgsfield_video.py b/tools/video/higgsfield_video.py index 95024d3b..f9ca67d2 100644 --- a/tools/video/higgsfield_video.py +++ b/tools/video/higgsfield_video.py @@ -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"], diff --git a/tools/video/runway_video.py b/tools/video/runway_video.py index f1d31fad..3e836bc4 100644 --- a/tools/video/runway_video.py +++ b/tools/video/runway_video.py @@ -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")