mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-10 08:32:09 +00:00
feat(video_gen): wire Nous subscription override into hermes tools UX
Add the same managed-gateway UX that image_gen already has: - TOOL_CATEGORIES['video_gen'] gets a 'Nous Subscription' provider row with managed_nous_feature='video_gen' + video_gen_plugin_name='fal' - NousSubscriptionFeatures gains a video_gen property + feature state computation (managed/active/available using the fal-queue gateway) - _GATEWAY_TOOL_LABELS, _GATEWAY_DIRECT_LABELS, _ALL_GATEWAY_KEYS, _get_gateway_direct_credentials, opted_in all include video_gen - apply_nous_managed_defaults and apply_gateway_defaults handle video_gen - _is_toolset_satisfied checks Nous features for video_gen - _is_provider_active detects managed video_gen (use_gateway + fal provider) - _select_plugin_video_gen_provider accepts use_gateway kwarg, propagated from all 4 call sites in _configure_provider when managed_feature is set - hermes setup status shows 'Video Generation (FAL via Nous subscription)' Users on a Nous subscription can now pick 'Nous Subscription' under hermes tools → Video Generation, which sets video_gen.provider=fal + video_gen.use_gateway=true. The FAL plugin's _resolve_managed_fal_video_gateway then routes through the managed queue gateway — no FAL_KEY needed.
This commit is contained in:
parent
b6294ea9f1
commit
a4c18f65d4
7 changed files with 111 additions and 34 deletions
|
|
@ -71,12 +71,16 @@ class NousSubscriptionFeatures:
|
|||
def browser(self) -> NousFeatureState:
|
||||
return self.features["browser"]
|
||||
|
||||
@property
|
||||
def video_gen(self) -> NousFeatureState:
|
||||
return self.features["video_gen"]
|
||||
|
||||
@property
|
||||
def modal(self) -> NousFeatureState:
|
||||
return self.features["modal"]
|
||||
|
||||
def items(self) -> Iterable[NousFeatureState]:
|
||||
ordered = ("web", "image_gen", "tts", "browser", "modal")
|
||||
ordered = ("web", "image_gen", "video_gen", "tts", "browser", "modal")
|
||||
for key in ordered:
|
||||
yield self.features[key]
|
||||
|
||||
|
|
@ -255,6 +259,7 @@ def get_nous_subscription_features(
|
|||
|
||||
web_tool_enabled = _toolset_enabled(config, "web")
|
||||
image_tool_enabled = _toolset_enabled(config, "image_gen")
|
||||
video_tool_enabled = _toolset_enabled(config, "video_gen")
|
||||
tts_tool_enabled = _toolset_enabled(config, "tts")
|
||||
browser_tool_enabled = _toolset_enabled(config, "browser")
|
||||
modal_tool_enabled = _toolset_enabled(config, "terminal")
|
||||
|
|
@ -289,6 +294,8 @@ def get_nous_subscription_features(
|
|||
browser_use_gateway = _uses_gateway(browser_cfg)
|
||||
image_gen_cfg = config.get("image_gen") if isinstance(config.get("image_gen"), dict) else {}
|
||||
image_use_gateway = _uses_gateway(image_gen_cfg)
|
||||
video_gen_cfg = config.get("video_gen") if isinstance(config.get("video_gen"), dict) else {}
|
||||
video_use_gateway = _uses_gateway(video_gen_cfg)
|
||||
|
||||
direct_exa = bool(get_env_value("EXA_API_KEY"))
|
||||
direct_firecrawl = bool(get_env_value("FIRECRAWL_API_KEY") or get_env_value("FIRECRAWL_API_URL"))
|
||||
|
|
@ -296,6 +303,7 @@ def get_nous_subscription_features(
|
|||
direct_tavily = bool(get_env_value("TAVILY_API_KEY"))
|
||||
direct_searxng = bool(get_env_value("SEARXNG_URL"))
|
||||
direct_fal = fal_key_is_configured()
|
||||
direct_fal_video = direct_fal # same FAL_KEY; separate var so use_gateway is independent
|
||||
direct_openai_tts = bool(resolve_openai_audio_api_key())
|
||||
direct_elevenlabs = bool(get_env_value("ELEVENLABS_API_KEY"))
|
||||
direct_camofox = bool(get_env_value("CAMOFOX_URL"))
|
||||
|
|
@ -311,6 +319,8 @@ def get_nous_subscription_features(
|
|||
direct_tavily = False
|
||||
if image_use_gateway:
|
||||
direct_fal = False
|
||||
if video_use_gateway:
|
||||
direct_fal_video = False
|
||||
if tts_use_gateway:
|
||||
direct_openai_tts = False
|
||||
direct_elevenlabs = False
|
||||
|
|
@ -320,6 +330,8 @@ def get_nous_subscription_features(
|
|||
|
||||
managed_web_available = managed_tools_flag and nous_auth_present and is_managed_tool_gateway_ready("firecrawl")
|
||||
managed_image_available = managed_tools_flag and nous_auth_present and is_managed_tool_gateway_ready("fal-queue")
|
||||
# Video gen uses the same fal-queue gateway as image gen.
|
||||
managed_video_available = managed_image_available
|
||||
managed_tts_available = managed_tools_flag and nous_auth_present and is_managed_tool_gateway_ready("openai-audio")
|
||||
managed_browser_available = managed_tools_flag and nous_auth_present and is_managed_tool_gateway_ready("browser-use")
|
||||
managed_modal_available = managed_tools_flag and nous_auth_present and is_managed_tool_gateway_ready("modal")
|
||||
|
|
@ -357,6 +369,10 @@ def get_nous_subscription_features(
|
|||
image_active = bool(image_tool_enabled and (image_managed or direct_fal))
|
||||
image_available = bool(managed_image_available or direct_fal)
|
||||
|
||||
video_managed = video_tool_enabled and managed_video_available and not direct_fal_video
|
||||
video_active = bool(video_tool_enabled and (video_managed or direct_fal_video))
|
||||
video_available = bool(managed_video_available or direct_fal_video)
|
||||
|
||||
tts_current_provider = tts_provider or "edge"
|
||||
tts_managed = (
|
||||
tts_tool_enabled
|
||||
|
|
@ -451,6 +467,18 @@ def get_nous_subscription_features(
|
|||
current_provider="FAL" if direct_fal else ("Nous Subscription" if image_managed else ""),
|
||||
explicit_configured=direct_fal,
|
||||
),
|
||||
"video_gen": NousFeatureState(
|
||||
key="video_gen",
|
||||
label="Video generation",
|
||||
included_by_default=False,
|
||||
available=video_available,
|
||||
active=video_active,
|
||||
managed_by_nous=video_managed,
|
||||
direct_override=video_active and not video_managed,
|
||||
toolset_enabled=video_tool_enabled,
|
||||
current_provider="FAL" if direct_fal_video else ("Nous Subscription" if video_managed else ""),
|
||||
explicit_configured=direct_fal_video,
|
||||
),
|
||||
"tts": NousFeatureState(
|
||||
key="tts",
|
||||
label="OpenAI TTS",
|
||||
|
|
@ -561,6 +589,9 @@ def apply_nous_managed_defaults(
|
|||
if "image_gen" in selected_toolsets and not fal_key_is_configured():
|
||||
changed.add("image_gen")
|
||||
|
||||
if "video_gen" in selected_toolsets and not fal_key_is_configured():
|
||||
changed.add("video_gen")
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
|
|
@ -571,6 +602,7 @@ def apply_nous_managed_defaults(
|
|||
_GATEWAY_TOOL_LABELS = {
|
||||
"web": "Web search & extract (Firecrawl)",
|
||||
"image_gen": "Image generation (FAL)",
|
||||
"video_gen": "Video generation (FAL)",
|
||||
"tts": "Text-to-speech (OpenAI TTS)",
|
||||
"browser": "Browser automation (Browser Use)",
|
||||
}
|
||||
|
|
@ -578,6 +610,7 @@ _GATEWAY_TOOL_LABELS = {
|
|||
|
||||
def _get_gateway_direct_credentials() -> Dict[str, bool]:
|
||||
"""Return a dict of tool_key -> has_direct_credentials."""
|
||||
fal_direct = fal_key_is_configured()
|
||||
return {
|
||||
"web": bool(
|
||||
get_env_value("FIRECRAWL_API_KEY")
|
||||
|
|
@ -586,7 +619,8 @@ def _get_gateway_direct_credentials() -> Dict[str, bool]:
|
|||
or get_env_value("TAVILY_API_KEY")
|
||||
or get_env_value("EXA_API_KEY")
|
||||
),
|
||||
"image_gen": fal_key_is_configured(),
|
||||
"image_gen": fal_direct,
|
||||
"video_gen": fal_direct,
|
||||
"tts": bool(
|
||||
resolve_openai_audio_api_key()
|
||||
or get_env_value("ELEVENLABS_API_KEY")
|
||||
|
|
@ -601,11 +635,12 @@ def _get_gateway_direct_credentials() -> Dict[str, bool]:
|
|||
_GATEWAY_DIRECT_LABELS = {
|
||||
"web": "Firecrawl/Exa/Parallel/Tavily key",
|
||||
"image_gen": "FAL key",
|
||||
"video_gen": "FAL key",
|
||||
"tts": "OpenAI/ElevenLabs key",
|
||||
"browser": "Browser Use/Browserbase key",
|
||||
}
|
||||
|
||||
_ALL_GATEWAY_KEYS = ("web", "image_gen", "tts", "browser")
|
||||
_ALL_GATEWAY_KEYS = ("web", "image_gen", "video_gen", "tts", "browser")
|
||||
|
||||
|
||||
def get_gateway_eligible_tools(
|
||||
|
|
@ -646,6 +681,7 @@ def get_gateway_eligible_tools(
|
|||
opted_in = {
|
||||
"web": _uses_gateway(config.get("web")),
|
||||
"image_gen": _uses_gateway(config.get("image_gen")),
|
||||
"video_gen": _uses_gateway(config.get("video_gen")),
|
||||
"tts": _uses_gateway(config.get("tts")),
|
||||
"browser": _uses_gateway(config.get("browser")),
|
||||
}
|
||||
|
|
@ -714,6 +750,15 @@ def apply_gateway_defaults(
|
|||
image_cfg["use_gateway"] = True
|
||||
changed.add("image_gen")
|
||||
|
||||
if "video_gen" in tool_keys:
|
||||
video_cfg = config.get("video_gen")
|
||||
if not isinstance(video_cfg, dict):
|
||||
video_cfg = {}
|
||||
config["video_gen"] = video_cfg
|
||||
video_cfg["provider"] = "fal"
|
||||
video_cfg["use_gateway"] = True
|
||||
changed.add("video_gen")
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -454,22 +454,25 @@ def _print_setup_summary(config: dict, hermes_home):
|
|||
# Video generation — opt-in via `hermes tools` → Video Generation.
|
||||
# Only show the row when a plugin reports available so we don't badger
|
||||
# users who don't care about video gen with a "missing" status line.
|
||||
try:
|
||||
from agent.video_gen_registry import list_providers as _list_video_providers
|
||||
from hermes_cli.plugins import _ensure_plugins_discovered as _ensure_plugins
|
||||
_ensure_plugins()
|
||||
_video_backend = None
|
||||
for _vp in _list_video_providers():
|
||||
try:
|
||||
if _vp.is_available():
|
||||
_video_backend = _vp.display_name
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
_video_backend = None
|
||||
if _video_backend:
|
||||
tool_status.append((f"Video Generation ({_video_backend})", True, None))
|
||||
if subscription_features.video_gen.managed_by_nous:
|
||||
tool_status.append(("Video Generation (FAL via Nous subscription)", True, None))
|
||||
else:
|
||||
try:
|
||||
from agent.video_gen_registry import list_providers as _list_video_providers
|
||||
from hermes_cli.plugins import _ensure_plugins_discovered as _ensure_plugins
|
||||
_ensure_plugins()
|
||||
_video_backend = None
|
||||
for _vp in _list_video_providers():
|
||||
try:
|
||||
if _vp.is_available():
|
||||
_video_backend = _vp.display_name
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
_video_backend = None
|
||||
if _video_backend:
|
||||
tool_status.append((f"Video Generation ({_video_backend})", True, None))
|
||||
|
||||
# TTS — show configured provider
|
||||
tts_provider = cfg_get(config, "tts", "provider", default="edge")
|
||||
|
|
|
|||
|
|
@ -339,11 +339,26 @@ TOOL_CATEGORIES = {
|
|||
"video_gen": {
|
||||
"name": "Video Generation",
|
||||
"icon": "🎬",
|
||||
# Providers list is intentionally empty — every video gen backend
|
||||
# is a plugin, surfaced by ``_plugin_video_gen_providers()`` and
|
||||
# injected by ``_visible_providers``. Mirrors the design we'll
|
||||
# converge image_gen toward.
|
||||
"providers": [],
|
||||
# "Nous Subscription" row mirrors the image_gen pattern — managed
|
||||
# FAL video generation billed via the Nous Portal. Plugin-backed
|
||||
# provider rows (FAL BYOK, xAI, …) are injected at runtime by
|
||||
# ``_plugin_video_gen_providers()`` in ``_visible_providers``.
|
||||
"providers": [
|
||||
{
|
||||
"name": "Nous Subscription",
|
||||
"badge": "subscription",
|
||||
"tag": "Managed FAL video generation billed to your subscription",
|
||||
"env_vars": [],
|
||||
"requires_nous_auth": True,
|
||||
"managed_nous_feature": "video_gen",
|
||||
"override_env_vars": ["FAL_KEY"],
|
||||
# The underlying plugin backend — when the user picks
|
||||
# "Nous Subscription" we set video_gen.provider = "fal"
|
||||
# and video_gen.use_gateway = True so the FAL plugin
|
||||
# routes through the managed queue gateway.
|
||||
"video_gen_plugin_name": "fal",
|
||||
},
|
||||
],
|
||||
},
|
||||
"x_search": {
|
||||
"name": "X (Twitter) Search",
|
||||
|
|
@ -1438,7 +1453,7 @@ def _toolset_has_keys(
|
|||
except Exception:
|
||||
return False
|
||||
|
||||
if ts_key in {"web", "image_gen", "tts", "browser"}:
|
||||
if ts_key in {"web", "image_gen", "video_gen", "tts", "browser"}:
|
||||
features = get_nous_subscription_features(config, force_fresh=force_fresh)
|
||||
feature = features.features.get(ts_key)
|
||||
if feature and (feature.available or feature.managed_by_nous):
|
||||
|
|
@ -2153,7 +2168,7 @@ def _is_provider_active(
|
|||
return isinstance(image_cfg, dict) and image_cfg.get("provider") == plugin_name
|
||||
|
||||
video_plugin_name = provider.get("video_gen_plugin_name")
|
||||
if video_plugin_name:
|
||||
if video_plugin_name and not provider.get("managed_nous_feature"):
|
||||
video_cfg = config.get("video_gen", {})
|
||||
return isinstance(video_cfg, dict) and video_cfg.get("provider") == video_plugin_name
|
||||
|
||||
|
|
@ -2172,6 +2187,15 @@ def _is_provider_active(
|
|||
if image_cfg.get("use_gateway") is not None and not is_truthy_value(image_cfg.get("use_gateway"), default=False):
|
||||
return False
|
||||
return feature.managed_by_nous
|
||||
if managed_feature == "video_gen":
|
||||
video_cfg = config.get("video_gen", {})
|
||||
if isinstance(video_cfg, dict):
|
||||
configured_provider = video_cfg.get("provider")
|
||||
if configured_provider not in {None, "", "fal"}:
|
||||
return False
|
||||
if video_cfg.get("use_gateway") is not None and not is_truthy_value(video_cfg.get("use_gateway"), default=False):
|
||||
return False
|
||||
return feature.managed_by_nous
|
||||
if provider.get("tts_provider"):
|
||||
return (
|
||||
feature.managed_by_nous
|
||||
|
|
@ -2505,14 +2529,14 @@ def _configure_videogen_model_for_plugin(plugin_name: str, config: dict) -> None
|
|||
_print_success(f" Model set to: {chosen}")
|
||||
|
||||
|
||||
def _select_plugin_video_gen_provider(plugin_name: str, config: dict) -> None:
|
||||
def _select_plugin_video_gen_provider(plugin_name: str, config: dict, *, use_gateway: bool = False) -> None:
|
||||
"""Persist a plugin-backed video generation provider selection."""
|
||||
vid_cfg = config.setdefault("video_gen", {})
|
||||
if not isinstance(vid_cfg, dict):
|
||||
vid_cfg = {}
|
||||
config["video_gen"] = vid_cfg
|
||||
vid_cfg["provider"] = plugin_name
|
||||
vid_cfg["use_gateway"] = False
|
||||
vid_cfg["use_gateway"] = use_gateway
|
||||
_print_success(f" video_gen.provider set to: {plugin_name}")
|
||||
_configure_videogen_model_for_plugin(plugin_name, config)
|
||||
|
||||
|
|
@ -2597,7 +2621,7 @@ def _configure_provider(
|
|||
# registry.
|
||||
video_plugin = provider.get("video_gen_plugin_name")
|
||||
if video_plugin:
|
||||
_select_plugin_video_gen_provider(video_plugin, config)
|
||||
_select_plugin_video_gen_provider(video_plugin, config, use_gateway=bool(managed_feature))
|
||||
return
|
||||
# Imagegen backends prompt for model selection after backend pick.
|
||||
backend = provider.get("imagegen_backend")
|
||||
|
|
@ -2676,7 +2700,7 @@ def _configure_provider(
|
|||
return
|
||||
video_plugin = provider.get("video_gen_plugin_name")
|
||||
if video_plugin:
|
||||
_select_plugin_video_gen_provider(video_plugin, config)
|
||||
_select_plugin_video_gen_provider(video_plugin, config, use_gateway=bool(managed_feature))
|
||||
return
|
||||
# Imagegen backends prompt for model selection after env vars are in.
|
||||
backend = provider.get("imagegen_backend")
|
||||
|
|
@ -2957,7 +2981,7 @@ def _reconfigure_provider(
|
|||
# Plugin-registered video_gen provider — same flow, different registry.
|
||||
video_plugin = provider.get("video_gen_plugin_name")
|
||||
if video_plugin:
|
||||
_select_plugin_video_gen_provider(video_plugin, config)
|
||||
_select_plugin_video_gen_provider(video_plugin, config, use_gateway=bool(managed_feature))
|
||||
return
|
||||
# Imagegen backends prompt for model selection on reconfig too.
|
||||
backend = provider.get("imagegen_backend")
|
||||
|
|
@ -2997,7 +3021,7 @@ def _reconfigure_provider(
|
|||
# Plugin-registered video_gen provider — same flow, different registry.
|
||||
video_plugin = provider.get("video_gen_plugin_name")
|
||||
if video_plugin:
|
||||
_select_plugin_video_gen_provider(video_plugin, config)
|
||||
_select_plugin_video_gen_provider(video_plugin, config, use_gateway=bool(managed_feature))
|
||||
return
|
||||
|
||||
backend = provider.get("imagegen_backend")
|
||||
|
|
|
|||
|
|
@ -440,6 +440,7 @@ class TestBuildNousSubscriptionPrompt:
|
|||
features={
|
||||
"web": NousFeatureState("web", "Web tools", True, True, True, True, False, True, "firecrawl"),
|
||||
"image_gen": NousFeatureState("image_gen", "Image generation", True, True, True, True, False, True, "Nous Subscription"),
|
||||
"video_gen": NousFeatureState("video_gen", "Video generation", False, False, False, False, False, False, ""),
|
||||
"tts": NousFeatureState("tts", "OpenAI TTS", True, True, True, True, False, True, "OpenAI TTS"),
|
||||
"browser": NousFeatureState("browser", "Browser automation", True, True, True, True, False, True, "Browser Use"),
|
||||
"modal": NousFeatureState("modal", "Modal execution", False, True, False, False, False, True, "local"),
|
||||
|
|
@ -464,6 +465,7 @@ class TestBuildNousSubscriptionPrompt:
|
|||
features={
|
||||
"web": NousFeatureState("web", "Web tools", True, False, False, False, False, True, ""),
|
||||
"image_gen": NousFeatureState("image_gen", "Image generation", True, False, False, False, False, True, ""),
|
||||
"video_gen": NousFeatureState("video_gen", "Video generation", False, False, False, False, False, False, ""),
|
||||
"tts": NousFeatureState("tts", "OpenAI TTS", True, False, False, False, False, True, ""),
|
||||
"browser": NousFeatureState("browser", "Browser automation", True, False, False, False, False, True, ""),
|
||||
"modal": NousFeatureState("modal", "Modal execution", False, False, False, False, False, True, ""),
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ def test_get_gateway_eligible_tools_ignores_quoted_false_opt_in(monkeypatch):
|
|||
monkeypatch.setattr(
|
||||
ns,
|
||||
"_get_gateway_direct_credentials",
|
||||
lambda: {"web": True, "image_gen": False, "tts": False, "browser": False},
|
||||
lambda: {"web": True, "image_gen": False, "video_gen": False, "tts": False, "browser": False},
|
||||
)
|
||||
|
||||
unconfigured, has_direct, already_managed = ns.get_gateway_eligible_tools(
|
||||
|
|
@ -230,4 +230,4 @@ def test_get_gateway_eligible_tools_ignores_quoted_false_opt_in(monkeypatch):
|
|||
|
||||
assert "web" in has_direct
|
||||
assert "web" not in already_managed
|
||||
assert set(unconfigured) == {"image_gen", "tts", "browser"}
|
||||
assert set(unconfigured) == {"image_gen", "video_gen", "tts", "browser"}
|
||||
|
|
|
|||
|
|
@ -498,6 +498,7 @@ def test_setup_summary_shows_camofox_when_browser_feature_is_camofox(tmp_path, m
|
|||
features={
|
||||
"web": NousFeatureState("web", "Web tools", True, False, False, False, False, True, ""),
|
||||
"image_gen": NousFeatureState("image_gen", "Image generation", True, False, False, False, False, True, ""),
|
||||
"video_gen": NousFeatureState("video_gen", "Video generation", False, False, False, False, False, False, ""),
|
||||
"tts": NousFeatureState("tts", "OpenAI TTS", True, False, False, False, False, True, ""),
|
||||
"browser": NousFeatureState("browser", "Browser automation", True, True, True, False, True, True, "Camofox"),
|
||||
"modal": NousFeatureState("modal", "Modal execution", False, False, False, False, False, True, "local"),
|
||||
|
|
@ -525,6 +526,7 @@ def test_setup_summary_does_not_mark_incomplete_browserbase_as_available(tmp_pat
|
|||
features={
|
||||
"web": NousFeatureState("web", "Web tools", True, False, False, False, False, True, ""),
|
||||
"image_gen": NousFeatureState("image_gen", "Image generation", True, False, False, False, False, True, ""),
|
||||
"video_gen": NousFeatureState("video_gen", "Video generation", False, False, False, False, False, False, ""),
|
||||
"tts": NousFeatureState("tts", "OpenAI TTS", True, False, False, False, False, True, ""),
|
||||
"browser": NousFeatureState("browser", "Browser automation", True, False, False, False, False, True, "Browserbase"),
|
||||
"modal": NousFeatureState("modal", "Modal execution", False, False, False, False, False, True, "local"),
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ def test_show_status_reports_managed_nous_features(monkeypatch, capsys, tmp_path
|
|||
features={
|
||||
"web": NousFeatureState("web", "Web tools", True, True, True, True, False, True, "firecrawl"),
|
||||
"image_gen": NousFeatureState("image_gen", "Image generation", True, True, True, True, False, True, "Nous Subscription"),
|
||||
"video_gen": NousFeatureState("video_gen", "Video generation", False, False, False, False, False, False, ""),
|
||||
"tts": NousFeatureState("tts", "OpenAI TTS", True, True, True, True, False, True, "OpenAI TTS"),
|
||||
"browser": NousFeatureState("browser", "Browser automation", True, True, True, True, False, True, "Browser Use"),
|
||||
"modal": NousFeatureState("modal", "Modal execution", False, True, False, False, False, True, "local"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue