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

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

совет

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

Механизм обнаружения

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

  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,
    normalize_reference_images,
    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 capabilities(self) -> Dict[str, Any]:
        # Объявляет, поддерживает ли бэкенд image-to-image / редактирование.
        # Слой инструмента отражает это в динамической схеме, чтобы модель
        # знала, когда учитывается `image_url`. По умолчанию (если метод не задан)
        # — только текст: {"modalities": ["text"], "max_reference_images": 0}.
        return {"modalities": ["text", "image"], "max_reference_images": 4}

    def generate(
        self,
        prompt: str,
        aspect_ratio: str = DEFAULT_ASPECT_RATIO,
        *,
        image_url: Optional[str] = None,
        reference_image_urls: Optional[List[str]] = None,
        **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,
            )

        # Маршрутизация: если задан image_url (или reference_image_urls), это запрос
        # image-to-image / редактирования, иначе — text-to-image. Выбранный путь
        # сообщайте через поле `modality` в success_response.
        sources = []
        if image_url:
            sources.append(image_url)
        sources.extend(normalize_reference_images(reference_image_urls) or [])
        modality = "image" if sources else "text"

        # Приоритет выбора модели: переменная окружения → конфиг → значение по умолчанию.
        # Хороший пример — _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"])
            if modality == "image":
                result = client.edit(
                    prompt=prompt,
                    model=model_id,
                    image_urls=sources,
                )
            else:
                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,
                modality=modality,
            )
        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_namename.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 сама решает, как объяснить их пользователю.

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, вложение в Discord) понимает и 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-ключ.

Примеры реализаций

  • plugins/image_gen/openai/__init__.py — gpt-image-2 с тремя уровнями качества (low/medium/high), реализованными как три виртуальных идентификатора модели на одном API-бэкенде с разными параметрами quality. Хорошая иллюстрация многоуровневых моделей на едином бэкенде и приоритетов конфига в config.yaml.
  • plugins/image_gen/xai/__init__.py — Grok Imagine через xAI. Другая форма ответа (URL-вывод, более простой каталог).
  • plugins/image_gen/openai-codex/__init__.py — вариант через Responses API в стиле Codex, переиспользующий OpenAI SDK с другим базовым URL маршрутизации.

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

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

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

Связанные страницы

ESC