# OpenRouter Image Generation Provider

Full working implementation of an OpenRouter-based image generation backend for Hermes Agent. Uses OpenRouter's chat completions API with image-capable models.

## Key Design Decision: Chat API vs Images API

Unlike the built-in OpenAI plugin (which uses `client.images.generate()` → `/v1/images/generations`), OpenRouter does NOT expose a dedicated images endpoint. Instead, image-capable models like `google/gemini-3-pro-image` and `openai/gpt-5-image` output images through the standard `/api/v1/chat/completions` endpoint.

This means the `generate()` method must:
1. Call `client.chat.completions.create()` with the image-capable model
2. Parse the multimodal response for image data
3. Save the image locally

## Available Models (on OpenRouter, as of 2026-06)

| Model ID | Display Name | Speed | Notes |
|----------|-------------|-------|-------|
| `google/gemini-3-pro-image` | Nano Banana Pro | ~8s | Geminis 3 Pro, good photorealism |
| `google/gemini-3.1-flash-image` | Nano Banana 2 | ~3s | Faster, Flash-level quality |
| `openai/gpt-5-image` | GPT-5 Image | ~20s | OpenAI native image gen |
| `openai/gpt-5.4-image-2` | GPT-5.4 Image 2 | ~15s | Latest OpenAI, SOTA |
| `openai/gpt-5-image-mini` | GPT-5 Image Mini | ~10s | Cheaper, faster |

## Response Format Variations

OpenRouter models return images in different formats depending on the underlying provider:

### Google Gemini models
The response `choices[0].message.content` may be:
- A **list** of content parts, where image parts have `type: "image_url"` with `image_url.url` containing a base64 data URL or HTTP URL
- A **string** with markdown image embeds: `![image](https://...)`

### OpenAI GPT Image models
May return:
- Base64-encoded image data in content parts
- HTTP URLs to generated images
- `venus_multimodal_url` attribute on the message object (OpenRouter-specific)

## Plugin Files

### plugin.yaml
```yaml
name: openrouter
version: 1.0.0
description: "OpenRouter image generation backend — uses chat API with image-capable models (Gemini 3 Pro Image, GPT-5 Image, etc.)"
author: User
kind: backend
requires_env:
  - OPENROUTER_API_KEY
```

### __init__.py (abridged — see full file at ~/.hermes/plugins/image_gen/openrouter/__init__.py)

Key structural patterns:

```python
from agent.image_gen_provider import (
    ImageGenProvider, success_response, error_response,
    save_b64_image, save_url_image, resolve_aspect_ratio,
)

class OpenRouterImageGenProvider(ImageGenProvider):
    @property
    def name(self) -> str:
        return "openrouter"

    def is_available(self) -> bool:
        return bool(_get_openrouter_api_key())

    def generate(self, prompt, aspect_ratio, **kwargs):
        # 1. Resolve API key
        # 2. Create OpenAI client pointing at OpenRouter base URL
        client = OpenAI(
            base_url="https://openrouter.ai/api/v1",
            api_key=api_key,
        )
        # 3. Call chat completions with image-capable model
        response = client.chat.completions.create(
            model=api_model,
            messages=[{"role": "user", "content": full_prompt}],
        )
        # 4. Parse multimodal response for image data
        content = response.choices[0].message.content
        # Handle: list of parts, base64 data URLs, markdown embeds, raw URLs
```

## Configuration

```bash
# Add API key
hermes auth add openrouter --type api-key --api-key "sk-or-v1-..."

# Set as image gen provider
hermes config set image_gen.provider openrouter
hermes config set image_gen.model openrouter/gemini-3-pro-image

# Verify
hermes tools list | grep image_gen
```

## API Key Validation

Always test the key before configuring:
```bash
curl -s https://openrouter.ai/api/v1/auth/key \
  -H "Authorization: Bearer *** 
```

A working key returns JSON with credit/limit info. A 401 means the key is invalid, expired, or has the wrong format.

**Key format**: OpenRouter keys are `sk-or-v1-<hash>`. Other formats (e.g. `sk-af...`) will not work — these may be from a different service.

## Pitfalls Discovered

1. **Trailing newline kills auth**: When reading keys from files, `.strip()` is essential. A `\n` in the Authorization header value causes 401 "Missing Authentication header" — the header is technically malformed.

2. **Models endpoint is public**: `GET /api/v1/models` returns 200 even without auth. Don't use it to validate API keys.

3. **Google vs OpenAI output format**: Gemini models wrap images in `image_url` content parts; GPT Image models may use different structures. The parser must handle both.

4. **Size specification**: Different models use different size formats:
   - GPT Image: literal dimensions (`"1536x1024"`)
   - Gemini: aspect ratio enum (`"16:9"`, `"1:1"`)
   - The provider must translate Hermes's abstract ratios (landscape/square/portrait) to each model's native format.
