File size: 12,940 Bytes
6aa76ed | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 | ---
sidebar_position: 11
title: "Image Generation Provider Plugins"
description: "How to build an image-generation backend plugin for Hermes Agent"
---
# Building an Image Generation Provider Plugin
Image-gen provider plugins register a backend that services every `image_generate` tool call β DALLΒ·E, gpt-image, Grok, Flux, Imagen, Stable Diffusion, fal, Replicate, a local ComfyUI rig, anything. Built-in providers (OpenAI, OpenAI-Codex, xAI, FAL, Krea, DeepInfra, OpenRouter, Meta Model API) all ship as plugins. You can add a new one, or override a bundled one, by dropping a directory into `plugins/image_gen/<name>/`.
:::tip
Image-gen is one of several **backend plugins** Hermes supports. The others (with more specialized ABCs) are [Memory Provider Plugins](/developer-guide/memory-provider-plugin), [Context Engine Plugins](/developer-guide/context-engine-plugin), and [Model Provider Plugins](/developer-guide/model-provider-plugin). General tool/hook/CLI plugins live in [Build a Hermes Plugin](/developer-guide/plugins).
:::
## How discovery works
Hermes scans for image-gen backends in three places:
1. **Bundled** β `<repo>/plugins/image_gen/<name>/` (auto-loaded with `kind: backend`, always available)
2. **User** β `~/.hermes/plugins/image_gen/<name>/` (opt-in via `plugins.enabled`)
3. **Pip** β packages declaring a `hermes_agent.plugins` entry point
Each plugin's `register(ctx)` function calls `ctx.register_image_gen_provider(...)` β that puts it into the registry in `agent/image_gen_registry.py`. The active provider is picked by `image_gen.provider` in `config.yaml`; `hermes tools` walks users through selection.
The `image_generate` tool wrapper asks the registry for the active provider and dispatches there. If no provider is registered, the tool surfaces a helpful error pointing at `hermes tools`.
## Directory structure
```
plugins/image_gen/my-backend/
βββ __init__.py # ImageGenProvider subclass + register()
βββ plugin.yaml # Manifest with kind: backend
```
A bundled plugin is complete at this point. User plugins at `~/.hermes/plugins/image_gen/<name>/` need to be added to `plugins.enabled` in `config.yaml` (or run `hermes plugins enable <name>`).
## The ImageGenProvider ABC
Subclass `agent.image_gen_provider.ImageGenProvider`. The only required members are the `name` property and the `generate()` method β everything else has sane defaults:
```python
# 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:
# Stable id used in image_gen.provider config. Lowercase, no spaces.
return "my-backend"
@property
def display_name(self) -> str:
# Human label shown in `hermes tools`. Defaults to name.title() if omitted.
return "My Backend"
def is_available(self) -> bool:
# Return False if credentials or deps are missing.
# The tool's availability gate calls this before dispatch.
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]]:
# Catalog shown in `hermes tools` model picker.
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]:
# Metadata for the `hermes tools` picker β keys to prompt for at setup.
return {
"name": "My Backend",
"badge": "paid", # optional; shown as a short tag in the picker
"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]:
# Declare whether this backend supports image-to-image / editing.
# The tool layer surfaces this in the dynamic schema so the model
# knows when `image_url` is honored. Default (if you omit this) is
# text-only: {"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,
)
# Routing: if image_url (or reference_image_urls) is set, the call is
# an image-to-image / edit request; otherwise text-to-image. Report
# which path you took via the `modality` field of 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"
# Model selection precedence: env var β config β default. The helper
# _resolve_model() in the built-in openai plugin is a good reference.
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,
)
# Two shapes supported:
# - URL string: return it as `image`
# - base64 data: save under $HERMES_HOME/cache/images/ via 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:
"""Plugin entry point β called once at load time."""
ctx.register_image_gen_provider(MyBackendImageGenProvider())
```
## plugin.yaml
```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` is what routes the plugin to the image-gen registration path. `requires_env` is prompted during `hermes plugins install`.
## ABC reference
Full contract in `agent/image_gen_provider.py`. The methods you'll typically override:
| Member | Required | Default | Purpose |
|---|---|---|---|
| `name` | β
| β | Stable id used in `image_gen.provider` config |
| `display_name` | β | `name.title()` | Label shown in `hermes tools` |
| `is_available()` | β | `True` | Gate for missing creds/deps |
| `list_models()` | β | `[]` | Catalog for `hermes tools` model picker |
| `default_model()` | β | first from `list_models()` | Fallback when no model is configured |
| `get_setup_schema()` | β | minimal | Picker metadata + env-var prompts |
| `generate(prompt, aspect_ratio, **kwargs)` | β
| β | The call |
## Response format
`generate()` must return a dict built via `success_response()` or `error_response()`. Both live in `agent/image_gen_provider.py`.
**Success:**
```python
success_response(
image=<url-or-absolute-path>,
model=<model-id>,
prompt=<echoed-prompt>,
aspect_ratio="landscape" | "square" | "portrait",
provider=<your-provider-name>,
extra={...}, # optional backend-specific fields
)
```
**Error:**
```python
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>,
)
```
The tool wrapper JSON-serializes the dict and hands it to the LLM. Errors are surfaced as the tool result; the LLM decides how to explain them to the user.
## Handling base64 vs URL output
Some backends return image URLs (fal, Replicate); others return base64 payloads (OpenAI gpt-image-2). For the base64 case, use `save_b64_image()` β it writes to `$HERMES_HOME/cache/images/<prefix>_<timestamp>_<uuid>.<ext>` and returns the absolute `Path`. Pass that path (as `str`) as `image=` in `success_response()`. Gateway delivery (Telegram photo bubble, Discord attachment) recognizes both URLs and absolute paths.
## User overrides
Drop a user plugin at `~/.hermes/plugins/image_gen/<name>/` with the same `name` property as a bundled one and enable it via `hermes plugins enable <name>` β the registry is last-writer-wins, so your version replaces the built-in. Useful for pointing an `openai` plugin at a private proxy, or swapping in a custom model catalog.
## Testing
```bash
export HERMES_HOME=/tmp/hermes-imggen-test
mkdir -p $HERMES_HOME/plugins/image_gen/my-backend
# β¦copy __init__.py + plugin.yaml into that dirβ¦
export MY_BACKEND_API_KEY=your-test-key
hermes plugins enable my-backend
# Pick it as the active provider
echo "image_gen:" >> $HERMES_HOME/config.yaml
echo " provider: my-backend" >> $HERMES_HOME/config.yaml
# Exercise it
hermes -z "Generate an image of a corgi in a spacesuit"
```
Or interactively: `hermes tools` β "Image Generation" β select `my-backend` β enter API key if prompted.
## Reference implementations
- **`plugins/image_gen/openai/__init__.py`** β gpt-image-2 at low/medium/high tiers as three virtual model IDs sharing one API model with different `quality` params. Good example of tiered models under a single backend + config.yaml precedence chain.
- **`plugins/image_gen/xai/__init__.py`** β Grok Imagine via xAI. Different shape (URL output, simpler catalog).
- **`plugins/image_gen/openai-codex/__init__.py`** β same catalog as `openai`, but authenticated with the ChatGPT/Codex OAuth token and posted with plain `httpx` to the Codex backend's native `images/generations` / `images/edits` endpoints. Good example of a provider that fetches remote source images client-side and inlines them as data URLs, and that reports backend-returned metadata separately from the request.
## Distribute via pip
```toml
# pyproject.toml
[project.entry-points."hermes_agent.plugins"]
my-backend-imggen = "my_backend_imggen_package"
```
`my_backend_imggen_package` must expose a top-level `register` function. See [Distribute via pip](/developer-guide/plugins#distribute-via-pip) in the general plugin guide for the full setup.
## Related pages
- [Image Generation](/user-guide/features/image-generation) β user-facing feature documentation
- [Plugins overview](/user-guide/features/plugins) β all plugin types at a glance
- [Build a Hermes Plugin](/developer-guide/plugins) β general tools/hooks/slash commands guide
|