Files
hermes-agent/tools/image_generation_managed.py
teknium1 294421fdfb feat(image_gen): route the managed selection by model id across FAL, Krea and Portal
`image_gen.provider: nous` fronts three gateways; the stored model id now says which
one serves a request (tools/image_generation_managed.py): FAL catalog ids → FAL gateway,
Krea plugin ids → Krea gateway, anything else → the Nous Portal image plugin. Before,
a Portal model id under the managed selection fell through to FAL, which warned about
an unknown model and billed a FLUX default instead.

The same module builds the managed row's union catalog with each model listed once
(FAL wins identical/cross-id duplicates; Krea 2 appears natively, never as the FAL or
Portal copy), so the picker and the runtime agree on one mapping.
2026-09-16 16:42:47 -07:00

85 lines
3.5 KiB
Python

"""Managed ("Nous Subscription") image generation: one picker row, one model catalog.
Three gateways sit behind the single stored selection ``image_gen.provider: nous``; the stored
``image_gen.model`` decides which one serves a request:
* ``fal`` — the FAL managed gateway, every id in the in-tree ``FAL_MODELS`` catalog;
* ``krea`` — the Krea managed gateway, the ``plugins/image_gen/krea`` model ids;
* ``portal`` — Nous Portal chat-completions image models (``plugins/image_gen/openrouter``'s
``nous`` provider), anything else.
Before this module the second and third gateways each had their own picker row that also
wrote ``provider: nous`` — every managed row read "active" at once and picking the Portal row
silently generated on FAL.
"""
from __future__ import annotations
from typing import Any, Dict, Optional, Tuple
FAL, KREA, PORTAL = "fal", "krea", "portal"
# Portal ids that are the same model as a FAL or Krea catalog entry. FAL wins (it is the
# gateway the free tool pool funds); Krea 2 goes through its native gateway, never FAL or the
# Portal. Only cross-id duplicates live here — identical ids collapse by key.
_PORTAL_DUPLICATE_OF = {
"openai/gpt-image-2": "fal-ai/gpt-image-2",
"google/gemini-3-pro-image": "fal-ai/nano-banana-pro",
"google/gemini-3.1-flash-image": "fal-ai/nano-banana-2",
"google/gemini-3.1-flash-lite-image": "google/nano-banana-2-lite",
"krea/krea-2-medium": "krea-2-medium",
"krea/krea-2-medium-turbo": "krea-2-medium-turbo",
"krea/krea-2-large": "krea-2-large",
}
_FAL_KREA_PREFIX = "fal-ai/krea/"
def managed_backend_for_model(model_id: Optional[str]) -> str:
"""Gateway that serves ``model_id`` under the managed selection (unset → FAL default)."""
from plugins.image_gen.krea import KREA_MODEL_IDS
from tools.image_generation_catalog import FAL_MODELS
candidate = model_id.strip() if isinstance(model_id, str) else ""
if not candidate or candidate in FAL_MODELS:
return FAL
if candidate in KREA_MODEL_IDS:
return KREA
return PORTAL
def _plugin_rows(name: str) -> list:
"""``list_models()`` of a registered image gen plugin; ``[]`` when unavailable."""
from tools.image_generation_tool import _get_plugin_provider
try:
provider = _get_plugin_provider(name)
return list(provider.list_models() or []) if provider is not None else []
except Exception: # noqa: BLE001 - a broken plugin must not empty the whole picker
return []
def managed_image_catalog(
*, include_krea: bool = True, include_portal: bool = True,
) -> Tuple[Dict[str, Dict[str, Any]], str]:
"""``({model_id: metadata}, default_model)`` for the managed row's model picker.
FAL catalog first (minus the Krea-on-FAL entries), then native Krea, then Portal models
that are not another entry in disguise. Every row carries ``backend`` so callers can hide
the gateways an account is not entitled to.
"""
from tools.image_generation_catalog import DEFAULT_MODEL, FAL_MODELS
catalog: Dict[str, Dict[str, Any]] = {
mid: {**meta, "backend": FAL} for mid, meta in FAL_MODELS.items()
if not mid.startswith(_FAL_KREA_PREFIX)}
if include_krea:
for row in _plugin_rows("krea"):
catalog[row["id"]] = {**row, "backend": KREA}
if include_portal:
for row in _plugin_rows("nous"):
mid = row["id"]
if mid in catalog or _PORTAL_DUPLICATE_OF.get(mid) in catalog:
continue
catalog[mid] = {**row, "backend": PORTAL}
return catalog, DEFAULT_MODEL