Создание плагина провайдера генерации изображений

Плагины провайдеров генерации изображений регистрируют бэкенд, который обрабатывает каждый вызов инструмента image_generate — DALL·E, gpt-image, Grok, Flux, Imagen, Stable Diffusion, fal, Replicate, локальная установка ComfyUI, всё что угодно. Встроенные провайдеры (OpenAI, OpenAI-Codex, xAI) поставляются как плагины. Вы можете добавить новый или переопределить встроенный, поместив директорию в plugins/image_gen/<name>/.

Генерация изображений — один из нескольких **backend-плагинов**, которые поддерживает Hermes. Другие (с более специализированными ABC) — это Плагины провайдеров памяти, Плагины контекстных движков и Плагины провайдеров моделей. Общие плагины инструментов/хуков/CLI описаны в Создание плагина Hermes.

Как работает обнаружение

Hermes сканирует бэкенды генерации изображений в трёх местах:

  1. Встроенные<repo>/plugins/image_gen/<name>/ (автозагружаются с kind: backend, всегда доступны)

  2. Пользовательские~/.hermes/plugins/image_gen/<name>/ (включаются через plugins.enabled)

  3. Pip — пакеты, объявляющие точку входа hermes_agent.plugins

Функция register(ctx) каждого плагина вызывает ctx.register_image_gen_provider(...) — это помещает его в реестр в agent/image_gen_registry.py. Активный провайдер выбирается параметром image_gen.provider в config.yaml; hermes tools проводит пользователя через процесс выбора.

Обёртка инструмента image_generate запрашивает у реестра активный провайдер и направляет вызов туда. Если провайдер не зарегистрирован, инструмент выводит понятную ошибку со ссылкой на hermes tools.

Структура директории

plugins/image_gen/my-backend/
├── __init__.py      # Подкласс ImageGenProvider + register()
└── plugin.yaml      # Манифест с kind: backend

Встроенный плагин на этом этапе полностью готов. Пользовательские плагины в ~/.hermes/plugins/image_gen/<name>/ нужно добавить в plugins.enabled в config.yaml (или выполнить hermes plugins enable <name>).

ABC ImageGenProvider

Унаследуйтесь от agent.image_gen_provider.ImageGenProvider. Обязательными элементами являются только свойство name и метод generate() — всё остальное имеет разумные значения по умолчанию:

# plugins/image_gen/my-backend/__init__.py
from typing import Any, Dict, List, Optional
import os

from agent.image_gen_provider import (
    DEFAULT_ASPECT_RATIO,
    ImageGenProvider,
    error_response,
    resolve_aspect_ratio,
    save_b64_image,
    success_response,
)


class MyBackendImageGenProvider(ImageGenProvider):
    @property
    def name(self) -> str:
        # Стабильный идентификатор, используемый в конфигурации image_gen.provider. Нижний регистр, без пробелов.
        return "my-backend"

    @property
    def display_name(self) -> str:
        # Человекочитаемая метка, показываемая в `hermes tools`. По умолчанию name.title().
        return "My Backend"

    def is_available(self) -> bool:
        # Возвращайте False, если отсутствуют учётные данные или зависимости.
        # Шлюз доступности инструмента вызывает этот метод перед отправкой.
        if not os.environ.get("MY_BACKEND_API_KEY"):
            return False
        try:
            import my_backend_sdk  # noqa: F401
        except ImportError:
            return False
        return True

    def list_models(self) -> List[Dict[str, Any]]:
        # Каталог, показываемый в выборе модели `hermes tools`.
        return [
            {
                "id": "my-model-fast",
                "display": "My Model (Fast)",
                "speed": "~5s",
                "strengths": "Quick iteration",
                "price": "$0.01/image",
            },
            {
                "id": "my-model-hq",
                "display": "My Model (HQ)",
                "speed": "~30s",
                "strengths": "Highest fidelity",
                "price": "$0.04/image",
            },
        ]

    def default_model(self) -> Optional[str]:
        return "my-model-fast"

    def get_setup_schema(self) -> Dict[str, Any]:
        # Метаданные для выбора в `hermes tools` — ключи, которые будут запрошены при настройке.
        return {
            "name": "My Backend",
            "badge": "paid",        # опционально; показывается как короткий тег в выборе
            "tag": "One-line description shown under the name",
            "env_vars": [
                {
                    "key": "MY_BACKEND_API_KEY",
                    "prompt": "My Backend API key",
                    "url": "https://my-backend.example.com/api-keys",
                },
            ],
        }

    def generate(
        self,
        prompt: str,
        aspect_ratio: str = DEFAULT_ASPECT_RATIO,
        **kwargs: Any,
    ) -> Dict[str, Any]:
        prompt = (prompt or "").strip()
        aspect_ratio = resolve_aspect_ratio(aspect_ratio)

        if not prompt:
            return error_response(
                error="Prompt is required",
                error_type="invalid_input",
                provider=self.name,
                prompt="",
                aspect_ratio=aspect_ratio,
            )

        # Приоритет выбора модели: переменная окружения → конфиг → значение по умолчанию.
        # Вспомогательный метод _resolve_model() во встроенном плагине openai — хороший пример.
        model_id = kwargs.get("model") or self.default_model() or "my-model-fast"

        try:
            import my_backend_sdk
            client = my_backend_sdk.Client(api_key=os.environ["MY_BACKEND_API_KEY"])
            result = client.generate(
                prompt=prompt,
                model=model_id,
                aspect_ratio=aspect_ratio,
            )

            # Поддерживается два формата:
            #   - URL-строка: возвращается как `image`
            #   - base64-данные: сохраняются в $HERMES_HOME/cache/images/ через save_b64_image()
            if result.get("image_b64"):
                path = save_b64_image(
                    result["image_b64"],
                    prefix=self.name,
                    extension="png",
                )
                image = str(path)
            else:
                image = result["image_url"]

            return success_response(
                image=image,
                model=model_id,
                prompt=prompt,
                aspect_ratio=aspect_ratio,
                provider=self.name,
            )
        except Exception as exc:
            return error_response(
                error=str(exc),
                error_type=type(exc).__name__,
                provider=self.name,
                model=model_id,
                prompt=prompt,
                aspect_ratio=aspect_ratio,
            )


def register(ctx) -> None:
    """Точка входа плагина — вызывается один раз при загрузке."""
    ctx.register_image_gen_provider(MyBackendImageGenProvider())

plugin.yaml

name: my-backend
version: 1.0.0
description: My image backend — text-to-image via My Backend SDK
author: Your Name
kind: backend
requires_env:
  - MY_BACKEND_API_KEY

kind: backend определяет маршрутизацию плагина по пути регистрации генерации изображений. requires_env запрашивается во время hermes plugins install.

Справочник по ABC

Полный контракт в agent/image_gen_provider.py. Методы, которые вы обычно будете переопределять:

Свойство Обязательно По умолчанию Назначение
name Стабильный идентификатор, используемый в конфигурации image_gen.provider
display_name name.title() Метка, отображаемая в hermes tools
is_available() True Проверка отсутствующих учётных данных/зависимостей
list_models() [] Каталог для выбора модели в hermes tools
default_model() первая из list_models() Резервное значение, если модель не настроена
get_setup_schema() минимальная Метаданные для выбора + подсказки по переменным окружения
generate(prompt, aspect_ratio, **kwargs) Вызов

Формат ответа

generate() должен возвращать словарь, созданный через success_response() или error_response(). Обе функции находятся в agent/image_gen_provider.py.

Успех:

success_response(
    image=<url-or-absolute-path>,
    model=<model-id>,
    prompt=<echoed-prompt>,
    aspect_ratio="landscape" | "square" | "portrait",
    provider=<your-provider-name>,
    extra={...},  # опциональные поля, специфичные для бэкенда
)

Ошибка:

error_response(
    error="human-readable message",
    error_type="provider_error" | "invalid_input" | "<exception class name>",
    provider=<your-provider-name>,
    model=<model-id>,
    prompt=<prompt>,
    aspect_ratio=<resolved aspect>,
)

Обёртка инструмента сериализует словарь в JSON и передаёт его LLM. Ошибки отображаются как результат инструмента; LLM решает, как объяснить их пользователю.

Обработка base64 и URL-вывода

Некоторые бэкенды возвращают URL-ссылки на изображения (fal, Replicate); другие — полезную нагрузку в base64 (OpenAI gpt-image-2). Для случая с base64 используйте save_b64_image() — она записывает файл в $HERMES_HOME/cache/images/<prefix>_<timestamp>_<uuid>.<ext> и возвращает абсолютный путь Path. Передайте этот путь (как str) в качестве image= в success_response(). Доставка через шлюз (Telegram photo bubble, Discord attachment) распознаёт как URL, так и абсолютные пути.

Пользовательские переопределения

Поместите пользовательский плагин в ~/.hermes/plugins/image_gen/<name>/ с тем же свойством name, что и у встроенного, и включите его через hermes plugins enable <name> — в реестре действует правило «последний записавший побеждает», поэтому ваша версия заменит встроенную. Полезно для направления плагина openai на приватный прокси или для замены каталога моделей.

Тестирование

export HERMES_HOME=/tmp/hermes-imggen-test
mkdir -p $HERMES_HOME/plugins/image_gen/my-backend
# …скопируйте __init__.py и plugin.yaml в эту директорию…

export MY_BACKEND_API_KEY=your-test-key
hermes plugins enable my-backend

# Выберите его как активный провайдер
echo "image_gen:" >> $HERMES_HOME/config.yaml
echo "  provider: my-backend" >> $HERMES_HOME/config.yaml

# Проверьте его работу
hermes -z "Generate an image of a corgi in a spacesuit"

Или интерактивно: hermes tools → «Image Generation» → выберите my-backend → введите API-ключ, если будет предложено.

Эталонные реализации

Распространение через pip

# pyproject.toml
[project.entry-points."hermes_agent.plugins"]
my-backend-imggen = "my_backend_imggen_package"

Пакет my_backend_imggen_package должен предоставлять функцию register на верхнем уровне. См. Распространение через pip в общем руководстве по плагинам для полной настройки.