feat(video_gen): route FAL video gen through managed Nous gateway
Wire plugins/video_gen/fal/__init__.py to use the same _ManagedFalSyncClient pattern that image gen already uses. Changes: - Add managed gateway resolution, client caching, and _submit_fal_video_request() that routes between direct FAL_KEY and Nous gateway modes - Update is_available() to return True when either FAL_KEY or the managed gateway is reachable - Update generate() to use submit+get handle pattern instead of fal_client.subscribe() directly - Fix happy-horse endpoint namespace: fal-ai/ → alibaba/ (matches the tool-gateway allowlist from fal-video-gen branch) - Surface actionable error on 4xx gateway rejections Tests: - 4 new tests in test_managed_media_gateways.py (gateway routing, client reuse, direct mode fallback, alibaba namespace) - Updated existing test_fal_plugin.py fixture to use submit/handle pattern and patch _resolve_managed_fal_video_gateway for isolation
This commit is contained in:
committed by
Siddharth Balyan
parent
5cd0673217
commit
d04b3c193e
@@ -305,3 +305,145 @@ def test_transcription_uses_model_specific_response_formats(monkeypatch, tmp_pat
|
||||
assert json_result["transcript"] == "hello from gpt-4o"
|
||||
assert json_capture["transcription_kwargs"]["response_format"] == "json"
|
||||
assert json_capture["close_calls"] == 1
|
||||
|
||||
|
||||
PLUGINS_DIR = Path(__file__).resolve().parents[2] / "plugins"
|
||||
|
||||
|
||||
def _load_video_gen_plugin(monkeypatch):
|
||||
"""Load the FAL video gen plugin in isolation."""
|
||||
_install_fake_tools_package()
|
||||
|
||||
# Also need the agent.video_gen_provider ABC
|
||||
agent_dir = Path(__file__).resolve().parents[2] / "agent"
|
||||
spec = spec_from_file_location(
|
||||
"agent.video_gen_provider",
|
||||
agent_dir / "video_gen_provider.py",
|
||||
)
|
||||
assert spec and spec.loader
|
||||
mod = module_from_spec(spec)
|
||||
sys.modules["agent.video_gen_provider"] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
# Load the plugin
|
||||
plugin_init = PLUGINS_DIR / "video_gen" / "fal" / "__init__.py"
|
||||
spec = spec_from_file_location("plugins.video_gen.fal", plugin_init)
|
||||
assert spec and spec.loader
|
||||
plugin_mod = module_from_spec(spec)
|
||||
sys.modules["plugins.video_gen.fal"] = plugin_mod
|
||||
spec.loader.exec_module(plugin_mod)
|
||||
return plugin_mod
|
||||
|
||||
|
||||
def test_video_gen_managed_fal_submit_uses_gateway(monkeypatch):
|
||||
"""Video gen routes through the managed gateway when FAL_KEY is absent."""
|
||||
captured = {}
|
||||
fake_fal = _install_fake_fal_client(captured)
|
||||
monkeypatch.delenv("FAL_KEY", raising=False)
|
||||
monkeypatch.setenv("FAL_QUEUE_GATEWAY_URL", "http://127.0.0.1:3009")
|
||||
monkeypatch.setenv("TOOL_GATEWAY_USER_TOKEN", "nous-video-token")
|
||||
|
||||
plugin = _load_video_gen_plugin(monkeypatch)
|
||||
|
||||
# Patch uuid for deterministic idempotency key
|
||||
monkeypatch.setattr(plugin.uuid, "uuid4", lambda: "video-submit-456")
|
||||
|
||||
plugin._submit_fal_video_request(
|
||||
"fal-ai/pixverse/v6/text-to-video",
|
||||
{"prompt": "a cat riding a bicycle", "duration": "5"},
|
||||
)
|
||||
|
||||
assert captured["submit_via"] == "managed_client"
|
||||
assert captured["client_key"] == "nous-video-token"
|
||||
assert captured["submit_url"] == "http://127.0.0.1:3009/fal-ai/pixverse/v6/text-to-video"
|
||||
assert captured["method"] == "POST"
|
||||
assert captured["arguments"] == {"prompt": "a cat riding a bicycle", "duration": "5"}
|
||||
assert captured["headers"] == {"x-idempotency-key": "video-submit-456"}
|
||||
assert captured["sync_client_inits"] == 1
|
||||
|
||||
|
||||
def test_video_gen_managed_client_reused_across_calls(monkeypatch):
|
||||
"""The managed video client is cached and reused across requests."""
|
||||
captured = {}
|
||||
_install_fake_fal_client(captured)
|
||||
monkeypatch.delenv("FAL_KEY", raising=False)
|
||||
monkeypatch.setenv("FAL_QUEUE_GATEWAY_URL", "http://127.0.0.1:3009")
|
||||
monkeypatch.setenv("TOOL_GATEWAY_USER_TOKEN", "nous-video-token")
|
||||
|
||||
plugin = _load_video_gen_plugin(monkeypatch)
|
||||
|
||||
plugin._submit_fal_video_request("fal-ai/pixverse/v6/text-to-video", {"prompt": "first"})
|
||||
first_client = captured["http_client"]
|
||||
plugin._submit_fal_video_request("fal-ai/pixverse/v6/text-to-video", {"prompt": "second"})
|
||||
|
||||
assert captured["sync_client_inits"] == 1
|
||||
assert captured["http_client"] is first_client
|
||||
|
||||
|
||||
def test_video_gen_direct_mode_when_fal_key_set(monkeypatch):
|
||||
"""When FAL_KEY is set and gateway not preferred, uses direct fal_client.submit."""
|
||||
captured = {}
|
||||
_install_fake_fal_client(captured)
|
||||
monkeypatch.setenv("FAL_KEY", "direct-fal-key-123")
|
||||
monkeypatch.delenv("FAL_QUEUE_GATEWAY_URL", raising=False)
|
||||
monkeypatch.delenv("TOOL_GATEWAY_USER_TOKEN", raising=False)
|
||||
|
||||
plugin = _load_video_gen_plugin(monkeypatch)
|
||||
monkeypatch.setattr(plugin.uuid, "uuid4", lambda: "direct-456")
|
||||
|
||||
# Trigger the lazy load so _fal_client is populated from our fake
|
||||
plugin._load_fal_client()
|
||||
|
||||
# In direct mode, fal_client.submit is the module-level function.
|
||||
# Our fake raises AssertionError from the managed path, so we need
|
||||
# to patch it to actually capture the call.
|
||||
direct_captured = {}
|
||||
|
||||
def direct_submit(endpoint, arguments=None, headers=None):
|
||||
direct_captured["endpoint"] = endpoint
|
||||
direct_captured["arguments"] = arguments
|
||||
direct_captured["headers"] = headers
|
||||
# Return a mock handle
|
||||
class FakeHandle:
|
||||
def get(self):
|
||||
return {"video": {"url": "https://fal.media/result.mp4"}}
|
||||
return FakeHandle()
|
||||
|
||||
plugin._fal_client.submit = direct_submit
|
||||
|
||||
plugin._submit_fal_video_request(
|
||||
"fal-ai/pixverse/v6/text-to-video",
|
||||
{"prompt": "test direct"},
|
||||
)
|
||||
|
||||
assert direct_captured["endpoint"] == "fal-ai/pixverse/v6/text-to-video"
|
||||
assert direct_captured["arguments"] == {"prompt": "test direct"}
|
||||
assert direct_captured["headers"] == {"x-idempotency-key": "direct-456"}
|
||||
# Managed client should NOT have been initialized
|
||||
assert "submit_via" not in captured
|
||||
|
||||
|
||||
def test_video_gen_happy_horse_uses_alibaba_namespace():
|
||||
"""Verify the happy-horse family uses alibaba/ not fal-ai/ endpoints."""
|
||||
_install_fake_tools_package()
|
||||
|
||||
# Load just the plugin module to check the catalog
|
||||
plugin_init = PLUGINS_DIR / "video_gen" / "fal" / "__init__.py"
|
||||
|
||||
agent_dir = Path(__file__).resolve().parents[2] / "agent"
|
||||
spec = spec_from_file_location(
|
||||
"agent.video_gen_provider",
|
||||
agent_dir / "video_gen_provider.py",
|
||||
)
|
||||
mod = module_from_spec(spec)
|
||||
sys.modules["agent.video_gen_provider"] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
spec = spec_from_file_location("plugins.video_gen.fal", plugin_init)
|
||||
plugin_mod = module_from_spec(spec)
|
||||
sys.modules["plugins.video_gen.fal"] = plugin_mod
|
||||
spec.loader.exec_module(plugin_mod)
|
||||
|
||||
hh = plugin_mod.FAL_FAMILIES["happy-horse"]
|
||||
assert hh["text_endpoint"] == "alibaba/happy-horse/text-to-video"
|
||||
assert hh["image_endpoint"] == "alibaba/happy-horse/image-to-video"
|
||||
|
||||
Reference in New Issue
Block a user