Generated banner for Nano Banana Pro Openrouter
Generated logo for Nano Banana Pro Openrouter

Nano Banana Pro Openrouter

GitHubOfficialFirst committed by nblogDistributed by GitHub

  • Skill
  • Content generator
  • Design and creative
  • MIT
  • Low risk
  • Has scripts

From the author

Generate or edit images via OpenRouter with the Gemini 3 Pro Image model. Use for prompt-only image generation, image edits, and multi-image compositing; supports 1K/2K/4K output.
— the description by nblog

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Nano Banana Pro OpenRouter provides an interface to generate, edit, and composite images using the google/gemini-3-pro-image-preview model through OpenRouter. It supports various resolutions and allows for system prompt customization via external templates.

What it does

  • Generates new images from text prompts.
  • Performs edits on single input images.
  • Composites multiple input images into a single output.
  • Supports 1K, 2K, and 4K resolution outputs.
  • Allows system prompt customization via a template file.
  • Handles multiple image outputs by appending indices to filenames.

When to use it

  • When you need to create new visual assets from text descriptions.
  • When you need to modify existing images using AI-driven edits.
  • When you need to combine multiple source images into a single composition.
  • When you require specific resolution outputs for your generated media.

Advantages

  • Flexible input handling for single or multiple source images.
  • Configurable resolution settings for output control.
  • Decoupled system prompt management via template files.
  • Standardized output reporting for integration with other tools.

Usage examples

“Generate an image of a futuristic city skyline at night.”
The agent executes the generation script and saves the resulting image file to the specified path.
“Edit this image to change the background to a forest.”
The agent processes the input image with the provided prompt and saves the modified version.
“Create a composite image using these two portraits.”
The agent merges the two provided source images into a single output file.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/github/nano-banana-pro-openrouter/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip nano-banana-pro-openrouter.zip -d .agents/skills

Documentation by nblog

Nano Banana Pro OpenRouter

Overview

Generate or edit images with OpenRouter using the google/gemini-3-pro-image-preview model. Support prompt-only generation, single-image edits, and multi-image composition.

Prompt-only generation
uv run {baseDir}/scripts/generate_image.py \
  --prompt "A cinematic sunset over snow-capped mountains" \
  --filename sunset.png
Edit a single image
uv run {baseDir}/scripts/generate_image.py \
  --prompt "Replace the sky with a dramatic aurora" \
  --input-image input.jpg \
  --filename aurora.png
Compose multiple images
uv run {baseDir}/scripts/generate_image.py \
  --prompt "Combine the subjects into a single studio portrait" \
  --input-image face1.jpg \
  --input-image face2.jpg \
  --filename composite.png

Resolution

  • Use --resolution with 1K, 2K, or 4K.

  • Default is 1K if not specified.

System prompt customization

The skill reads an optional system prompt from assets/SYSTEM_TEMPLATE. This allows you to customize the image generation behavior without modifying code.

Behavior and constraints

  • Accept up to 3 input images via repeated --input-image.

  • --filename accepts relative paths (saves to current directory) or absolute paths.

  • If multiple images are returned, append -1, -2, etc. to the filename.

  • Print MEDIA: <path> for each saved image. Do not read images back into the response.

Troubleshooting

If the script exits non-zero, check stderr against these common blockers:

SymptomResolution
OPENROUTER_API_KEY is not setAsk the user to set it. PowerShell: $env:OPENROUTER_API_KEY = "sk-or-..." / bash: export OPENROUTER_API_KEY="sk-or-..."
uv: command not found or not recognizedmacOS/Linux: <code>curl -LsSf https://astral.sh/uv/install.sh | sh</code>. Windows: <code>powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"</code>. Then restart the terminal.
AuthenticationError / HTTP 401Key is invalid or has no credits. Verify at https://openrouter.ai/settings/keys.

For transient errors (HTTP 429, network timeouts), retry once after 30 seconds. Do not retry the same error more than twice — surface the issue to the user instead.

Files

scripts/generate_image.py

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "openai",
# ]
# ///
"""
Generate or edit images via OpenRouter using openai-python.
"""

import argparse
import base64
import mimetypes
import os
from pathlib import Path

from openai import OpenAI


# Configuration
MAX_INPUT_IMAGES = 3
MIME_TO_EXT = {
    "image/png": ".png",
    "image/jpeg": ".jpg",
    "image/jpg": ".jpg",
    "image/webp": ".webp",
}


def parse_args():
    parser = argparse.ArgumentParser(description="Generate or edit images via OpenRouter.")
    parser.add_argument("--prompt", required=True, help="Prompt describing the desired image.")
    parser.add_argument("--filename", required=True, help="Output filename (relative to CWD).")
    parser.add_argument(
      "--resolution",
      type=str.upper,
      choices=["1K", "2K", "4K"],
      default="1K",
      help="Output resolution: 1K, 2K, or 4K.",
    )
    parser.add_argument(
      "--input-image",
      action="append",
      default=[],
      help=f"Optional input image path (repeatable, max {MAX_INPUT_IMAGES}).",
    )
    return parser.parse_args()


def require_api_key():
    api_key = os.environ.get("OPENROUTER_API_KEY")
    if not api_key:
        raise SystemExit("OPENROUTER_API_KEY is not set in the environment.")
    return api_key


def encode_image_to_data_url(path: Path) -> str:
    if not path.exists():
        raise SystemExit(f"Input image not found: {path}")
    mime, _ = mimetypes.guess_type(str(path))
    if not mime:
        mime = "image/png"
    data = path.read_bytes()
    encoded = base64.b64encode(data).decode("utf-8")
    return f"data:{mime};base64,{encoded}"


def build_message_content(prompt: str, input_images: list[str]) -> list[dict]:
    content: list[dict] = [{"type": "text", "text": prompt}]
    for image_path in input_images:
        data_url = encode_image_to_data_url(Path(image_path))
        content.append({"type": "image_url", "image_url": {"url": data_url}})
    return content


def parse_data_url(data_url: str) -> tuple[str, bytes]:
    if not data_url.startswith("data:") or ";base64," not in data_url:
        raise SystemExit("Image URL is not a base64 data URL.")
    header, encoded = data_url.split(",", 1)
    mime = header[5:].split(";", 1)[0]
    try:
        raw = base64.b64decode(encoded)
    except Exception as e:
        raise SystemExit(f"Failed to decode base64 image payload: {e}")
    return mime, raw


def resolve_output_path(filename: str, image_index: int, total_count: int, mime: str) -> Path:
    output_path = Path(filename)
    suffix = output_path.suffix

    # Validate/correct suffix matches MIME type
    expected_suffix = MIME_TO_EXT.get(mime, ".png")
    if suffix and suffix.lower() != expected_suffix.lower():
        print(f"Warning: filename extension '{suffix}' doesn't match returned MIME type '{mime}'. Using '{expected_suffix}' instead.")
        suffix = expected_suffix
    elif not suffix:
        suffix = expected_suffix

    # Single image: use original stem + corrected suffix
    if total_count <= 1:
        return output_path.with_suffix(suffix)

    # Multiple images: append numbering
    return output_path.with_name(f"{output_path.stem}-{image_index + 1}{suffix}")


def extract_image_url(image: dict | object) -> str | None:
    if isinstance(image, dict):
        return image.get("image_url", {}).get("url") or image.get("url")
    return None


def load_system_prompt():
    """Load system prompt from assets/SYSTEM_TEMPLATE if it exists and is not empty."""
    script_dir = Path(__file__).parent.parent
    template_path = script_dir / "assets" / "SYSTEM_TEMPLATE"

    if template_path.exists():
        content = template_path.read_text(encoding="utf-8").strip()
        if content:
            return content
    return None


def main():
    args = parse_args()

    if len(args.input_image) > MAX_INPUT_IMAGES:
        raise SystemExit(f"Too many input images: {len(args.input_image)} (max {MAX_INPUT_IMAGES}).")

    image_size = args.resolution

    client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=require_api_key())

    # Build messages with optional system prompt
    messages = []

    system_prompt = load_system_prompt()
    if system_prompt:
        messages.append({
            "role": "system",
            "content": system_prompt,
        })

    messages.append({
        "role": "user",
        "content": build_message_content(args.prompt, args.input_image),
    })

    response = client.chat.completions.create(
        model="google/gemini-3-pro-image-preview",
        messages=messages,
        extra_body={
            "modalities": ["image", "text"],
            # https://openrouter.ai/docs/guides/overview/multimodal/image-generation#image-configuration-options
            "image_config": {
                # "aspect_ratio": "16:9",
                "image_size": image_size,
            }
        },
    )

    message = response.choices[0].message
    images = getattr(message, "images", None)
    if not images:
        raise SystemExit("No images returned by the API.")

    # Create output directory once before processing images
    output_base_path = Path(args.filename)
    if output_base_path.parent and str(output_base_path.parent) != '.':
        output_base_path.parent.mkdir(parents=True, exist_ok=True)

    saved_paths = []
    for idx, image in enumerate(images):
        image_url = extract_image_url(image)
        if not image_url:
            raise SystemExit("Image payload missing image_url.url.")
        mime, raw = parse_data_url(image_url)
        output_path = resolve_output_path(args.filename, idx, len(images), mime)
        output_path.write_bytes(raw)
        saved_paths.append(output_path.resolve())

    for path in saved_paths:
        print(f"Saved image to: {path}")
        print(f"MEDIA: {path}")


if __name__ == "__main__":
    main()

Requirements and compatibility

Written for: GitHub Copilot

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs network access, Needs an API key, Needs Python

Security

What our scanner noticed· Low risk

  • Instructions that use the network
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

First committed by
GitHub avatar of nblognblog
Publisher
GitHub (Official)
Distributed by
GitHub
Source
github/awesome-copilot · skills/nano-banana-pro-openrouter · commit 797337f
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced
Contributors
nblog

AutoGineer is not affiliated with or endorsed by nblog or GitHub.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download nano-banana-pro-openrouter.zip