Skill: chunhualiao/glm-image (version 1.9.0)

--- name: glm-image description: Generate images using GLM-Image API. Use when the user wants to generate, create, or draw an image from a text prompt. Triggers on requests like "generate an image of...", "create a picture of...", "draw...", or any image generation request. --- # GLM-Image Generator Generate images from text prompts using the GLM-Image API. > **Attribution:** Based on [glm-image](https://github.com/ViffyGwaanl/glm-image) by ViffyGwaanl (MIT License). ## Setup This skill supports two providers. **You only need one.** ### Option A — GLM (BigModel / Zhipu AI) Requires `GLM_API_KEY` from https://open.bigmodel.cn → Console → API Keys ```bash export GLM_API_KEY=your-key # or add to ~/.openclaw/config.json: { "api_key": "your-key" } # or add GLM_API_KEY=your-key to .env ``` ### Option B — OpenRouter Requires `OPENROUTER_API_KEY` from https://openrouter.ai → Keys ```bash export OPENROUTER_API_KEY=your-key # or add to ~/.openclaw/config.json: { "openrouter_api_key": "your-key" } # or add OPENROUTER_API_KEY=your-key to .env ``` Default OpenRouter model: [REDACTED_SECRET]. Other options: `openai/gpt-5-image-mini`, `openai/gpt-5-image`, `google/gemini-2.5-flash-image-preview`. See full list at https://openrouter.ai/collections/image-models **Auto-detection:** if both keys are present, GLM is used. Override with `--provider openrouter`. ## Usage When a user requests image generation: **Step 0 — Verify at least one API key is configured** Run: ```bash python3 -c " import os, json, pathlib glm = bool(os.environ.get('GLM_API_KEY')) orouter = bool(os.environ.get('OPENROUTER_API_KEY')) if not glm and not orouter: for p in ['~/.openclaw/config.json', '~/.claude/config.json']: try: d = json.loads(pathlib.Path(p).expanduser().read_text()) if d.get('api_key'): glm = True if d.get('openrouter_api_key'): orouter = True except: pass keys = [] if glm: keys.append('GLM_API_KEY') if orouter: keys.append('OPENROUTER_API_KEY') print('FOUND: ' + ', '.join(keys) if keys else 'KEY_MISSING') " ``` If output is `KEY_MISSING`, tell the user: > "No API key is configured. This skill supports two providers — you only need one: > > **Option A — GLM (BigModel):** Get key at https://open.bigmodel.cn → Console → API Keys, then: > ``` > export GLM_API_KEY=your-key > ``` > > **Option B — OpenRouter:** Get key at https://openrouter.ai → Keys, then: > ``` > export OPENROUTER_API_KEY=your-key > ``` > > For either option you can also add the key to `~/.openclaw/config.json`: > ```json > { "api_key": "glm-key" } > { "openrouter_api_key": "openrouter-key" } > ```" Do not proceed until the user confirms a key is set. **Step 1 — Ask for language (MANDATORY, no exceptions)** Before running anything, ask: > "What language is your prompt in? Please choose: zh (Chinese), en (English), ja (Japanese), ko (Korean), fr (French), de (German), es (Spanish)." Do NOT infer language from the user's message language or any other signal. Do NOT default to any language. Do NOT proceed until the user explicitly states a language code. **Step 2 — Run the generation script** ```bash python3 scripts/generate.py "<prompt>" --language <code> ``` `--language` is required. The script will error if omitted. Other defaults: - Size: 1088x1920 (portrait HD) - Output: `output/` folder - No watermark **Step 3 — Return the result** Display the markdown image link and local file path. ## Generate Image ```bash python3 scripts/generate.py "<prompt>" --language <zh|en|ja|ko|fr|de|es> ``` Provider is auto-detected from available keys. Override explicitly: ```bash # Force OpenRouter with a specific model python3 scripts/generate.py "<prompt>" --language en --provider openrouter --model google/gemini-2.5-flash-image-preview # Force GLM python3 scripts/generate.py "<prompt>" --language zh --provider glm ``` ### Options - `--language`: **(Required)** Prompt language. Must be explicitly provided by the user. Supported: `zh` (Chinese), `en` (English), `ja` (Japanese), `ko` (Korean), `fr` (French), `de` (German), `es` (Spanish) - `--provider`: `glm` or `openrouter`. Auto-detected if omitted (GLM preferred when both keys present) - `--model`: OpenRouter model slug (default: [REDACTED_SECRET]). Ignored for GLM. See https://openrouter.ai/collections/image-models - `--size`: Image dimensions, GLM only (default: 1088x1920). Valid range: 512-2048px, multiples of 32 - `--output`: Output directory (default: output/) - `--quality`: Image quality, GLM only — "hd" or "standard" (default: hd) - `--watermark`: Enable watermark, GLM only ### Language Selection Rules - **Always ask explicitly.** Never guess from the user's message language. - **Never default.** If the user does not specify, ask again. - **Record as-typed.** Pass exactly what the user said (e.g., `zh`, `en`) — do not normalize. - Reason: GLM is a Chinese-native model; prompt language significantly affects output quality and style. ### Available Sizes - 1088x1920 (default, portrait HD) - 1920x1088 (landscape HD) - 1280x1280 (square) - 1568x1056, 1056x1568 - 1472x1088, 1088x1472 - 1728x960, 960x1728 ## Output Format After successful generation, display: 1. Local file path: `output/<timestamp>_<prompt>.png` 2. Markdown image link: `![<prompt>](<url>)` ## Agent Owner This skill is executed by the main OpenClaw agent session. The `generate.py` script runs as a shell command via the exec tool. No sub-agents are spawned. ## Success Criteria Image generation succeeds when: 1. Script exits with code 0 2. Image file saved to output/ directory 3. Markdown image link displayed to user Failure conditions: invalid API key, unsupported size, network timeout (120s), API quota exceeded. ## Edge Cases - Invalid size: must be 512-2048px in multiples of 32 — script will fail with API error - Long prompts: prompt truncated to 30 chars in filename (full prompt used for generation) - Network timeout: 120s API timeout, 60s download timeout — retry once if timeout - Missing API key: script exits with clear error message listing search locations - Chinese characters in prompt: supported, filename sanitized automatically ## Requirements - GLM API key configured (see Setup section above) - Python 3 with `requests` package (`pip install requests`)

path: CHANGELOG.md
content: # Changelog

## [1.9.0] - 2026-02-18
### Changed
- Default OpenRouter model changed from `openai/gpt-5-image-mini` to [REDACTED_SECRET] — better fallback when GLM API not present
- Margin guard relaxed: no longer mandates exact 80%/10% constraints; gives the model flexibility on dimensions while keeping comfortable margins
- Vertical layout recommended but not forced — long content benefits from tall format

## [1.8.0] - 2026-02-18
### Changed
- OpenRouter margin guard strengthened: from "5% margin / 50px" to "center 80% constraint with 10% empty border on all four sides" — more explicit instruction to prevent edge clipping on gpt-5-image-mini

## [1.7.0] - 2026-02-18
### Fixed
- OpenRouter: append margin guard instruction to every prompt — prevents content clipping at canvas edges (gpt-5-image-mini places labels/text flush against the edge with no padding)

## [1.6.0] - 2026-02-18
### Fixed
- OpenRouter response parsing: image URL was at `images[0]["image_url"]["url"]` (snake_case), not `imageUrl` (camelCase) — was crashing with KeyError on every generation
- Cost extraction: moved to `usage.cost` from main response body (immediate, no extra API call); generation endpoint kept as fallback only

## [1.5.0] - 2026-02-18
### Added
- Cost display at end of every run:
  - GLM: shows ¥ and ~$ from known pricing (¥0.10 standard / ¥0.20 HD) with console link
  - OpenRouter: fetches actual USD cost from `/api/v1/generation?id=<id>` endpoint; falls back to activity link
### Changed
- Default OpenRouter model: `google/gemini-2.5-flash-image-preview` → `openai/gpt-5-image-mini`
  (better text rendering for scrapbook-style prompts)

## [1.4.0] - 2026-02-18
### Added
- OpenRouter provider support: `--provider openrouter` uses `/api/v1/chat/completions`
  with `modalities: ["image"]`; returns base64 image decoded and saved locally
- `--provider` flag: auto-detects from available keys (GLM preferred); explicit override available
- `--model` flag: choose OpenRouter image model (default: google/gemini-2.5-flash-image-preview)
- `OPENROUTER_API_KEY` lookup in env, config.json (`openrouter_api_key`), and .env
- Setup section updated: two-provider docs with key registration links
- Step 0 now checks for either key; tells user both setup options if neither found

## [1.3.0] - 2026-02-18
### Added
- Setup section: documents all 4 key lookup locations with examples (env var, config.json, .env)
- Step 0 in Usage: agent checks for KEY_MISSING before proceeding, then tells user exactly how to configure in plain language (3 options with code examples)

## [1.2.0] - 2026-02-18
### Added
- Explicit language selection: `--language` flag (required) on `generate.py`
- Supported languages: zh, en, ja, ko, fr, de, es
- SKILL.md mandates asking the user for language choice before every generation — no defaulting, no inferring
- Language label printed in script output

## [1.1.0] - 2026-02-17
### Added
- skill.yml with display_name, attribution, triggers, permissions
- README.md
- Configuration section (GLM_API_KEY via TOOLS.md)
- Agent owner declaration
- Success criteria and edge cases
- Attribution to original author ViffyGwaanl
### Fixed
- Script now also checks ~/.openclaw/config.json for API key

## [1.0.0] - 2026-02-01
### Added
- Initial release by ViffyGwaanl
- generate.py with full GLM-Image API support


path: README.md
content: # GLM Image Generator

Generate images from text prompts using [GLM-Image](https://open.bigmodel.cn) (Zhipu AI) or [OpenRouter](https://openrouter.ai) image models.

> **Attribution:** Based on [glm-image](https://github.com/ViffyGwaanl/glm-image) by ViffyGwaanl (MIT License).

## Sample

**Multi-agent orchestration — scrapbook style (Chinese)**

Generated with [REDACTED_SECRET] via OpenRouter:

![Multi-agent scrapbook illustration](samples/multi-agent-scrapbook-zh.jpg)

## Setup

This skill supports two providers. You only need one.

**Option A — GLM (BigModel / Zhipu AI)**

```bash
export GLM_API_KEY=your-key
```

Get your key at https://open.bigmodel.cn → Console → API Keys

**Option B — OpenRouter**

```bash
export OPENROUTER_API_KEY=your-key
```

Get your key at https://openrouter.ai → Keys

Default model: [REDACTED_SECRET]. Override with `--model`.

If both keys are present, GLM is preferred. Override with `--provider openrouter`.

## Usage

```bash
# Auto-detect provider
python3 scripts/generate.py "a sunset over mountains" --language en

# Force OpenRouter with specific model
python3 scripts/generate.py "一只猫在月光下" --language zh --provider openrouter --model openai/gpt-5-image-mini

# Force GLM
python3 scripts/generate.py "a robot playing chess" --language en --provider glm
```

### Options

| Flag | Description |
|------|-------------|
| `--language` | **Required.** `zh` `en` `ja` `ko` `fr` `de` `es` |
| `--provider` | `glm` or `openrouter` (auto-detected if omitted) |
| `--model` | OpenRouter model slug (ignored for GLM) |
| `--size` | Image dimensions, GLM only (default: 1088x1920) |
| `--quality` | `hd` or `standard`, GLM only (default: hd) |
| `--output` | Output directory (default: `output/`) |
| `--watermark` | Enable watermark, GLM only |

## Output

Images saved to `output/` with timestamped filenames. Cost displayed after generation.

## Requirements

- Python 3
- `requests` library (`pip install requests`)


path: STATUS.json
content: stage: test
version: 1.1.0
attribution: https://github.com/ViffyGwaanl/glm-image

path: scripts/generate.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GLM-Image generation script for Claude Code skill.
Generates an image from a text prompt and outputs markdown-formatted result.

Supports two providers:
  - glm       : BigModel / Zhipu AI (requires GLM_API_KEY)
  - openrouter: OpenRouter image models (requires OPENROUTER_API_KEY)

Auto-detects provider from available keys unless --provider is specified.
"""

import os
import sys
import json
import base64
import datetime
import re
import argparse
import requests


SUPPORTED_LANGUAGES = {
    "zh": "Chinese",
    "en": "English",
    "ja": "Japanese",
    "ko": "Korean",
    "fr": "French",
    "de": "German",
    "es": "Spanish",
}

# Default OpenRouter image model. Override with --model.
# gemini-3-pro: strong image generation with good text rendering
OPENROUTER_DEFAULT_MODEL = "google/gemini-3-pro-image-preview"

# GLM-Image known pricing (source: BigModel API docs, as of Jan 2026)
GLM_PRICE_STANDARD_CNY = 0.10   # ¥ per image
GLM_PRICE_HD_CNY       = 0.20   # ¥ per image (HD quality)
CNY_TO_USD             = 0.138  # approximate; fluctuates


# ---------------------------------------------------------------------------
# Key loaders
# ---------------------------------------------------------------------------

def _load_key_from_env_and_configs(env_var: str, config_key: str) -> str | None:
    """Try env var → config files → .env files. Returns None if not found."""
    value = os.environ.get(env_var)
    if value:
        return value

    config_paths = [
        "config.json",
        os.path.expanduser("~/.openclaw/config.json"),
        os.path.expanduser("~/.claude/config.json"),
    ]
    for path in config_paths:
        if os.path.exists(path):
            try:
                with open(path) as f:
                    data = json.load(f)
                    if data.get(config_key):
                        return data[config_key]
            except Exception:
                pass

    script_dir = os.path.dirname(os.path.abspath(__file__))
    skill_dir = os.path.dirname(script_dir)
    env_paths = [
        os.path.join(skill_dir, ".env"),
        ".env",
        os.path.expanduser("~/.env"),
    ]
    for path in env_paths:
        if os.path.exists(path):
            try:
                with open(path) as f:
                    for line in f:
                        line = line.strip()
                        if line.startswith(f"{env_var}="):
                            val = line.split("=", 1)[1].strip().strip('"')
                            if val:
                                return val
            except Exception:
                pass

    return None


# ---------------------------------------------------------------------------
# Cost helpers
# ---------------------------------------------------------------------------

def fetch_openrouter_cost(generation_id: str, api_key: str) -> float | None:
    """
    Fallback: query OpenRouter's /api/v1/generation endpoint for cost in USD.
    Only used when cost wasn't in the main response's usage field.
    """
    try:
        url = f"https://openrouter.ai/api/v1/generation?id={generation_id}"
        headers = {"Authorization": f"Bearer {api_key}"}
        resp = requests.get(url, headers=headers, timeout=15)
        resp.raise_for_status()
        return resp.json().get("data", {}).get("total_cost")
    except Exception:
        return None


def glm_cost_str(quality: str) -> str:
    """Return formatted cost string for a GLM image generation."""
    cny = GLM_PRICE_HD_CNY if quality == "hd" else GLM_PRICE_STANDARD_CNY
    usd = cny * CNY_TO_USD
    return f"¥{cny:.2f} (~${usd:.4f}) — verify at https://open.bigmodel.cn/console"


def load_glm_key() -> str:
    key = _load_key_from_env_and_configs("GLM_API_KEY", "api_key")
    if not key:
        raise ValueError(
            "GLM_API_KEY not found.\n"
            "Get your key at https://open.bigmodel.cn → Console → API Keys\n"
            "Then set it via:\n"
            "  export GLM_API_KEY=your-key\n"
            "  or add to ~/.openclaw/config.json: {\"api_key\": \"your-key\"}\n"
            "  or add GLM_API_KEY=your-key to .env"
        )
    return key


def load_openrouter_key() -> str:
    key = _load_key_from_env_and_configs("OPENROUTER_API_KEY", "openrouter_api_key")
    if not key:
        raise ValueError(
            "OPENROUTER_API_KEY not found.\n"
            "Get your key at https://openrouter.ai → Keys\n"
            "Then set it via:\n"
            "  export OPENROUTER_API_KEY=your-key\n"
            "  or add to ~/.openclaw/config.json: {\"openrouter_api_key\": \"your-key\"}\n"
            "  or add OPENROUTER_API_KEY=your-key to .env"
        )
    return key


def detect_provider() -> str:
    """Return 'glm' or 'openrouter' based on which key is available."""
    if _load_key_from_env_and_configs("GLM_API_KEY", "api_key"):
        return "glm"
    if _load_key_from_env_and_configs("OPENROUTER_API_KEY", "openrouter_api_key"):
        return "openrouter"
    raise ValueError(
        "No API key found. Configure one of:\n"
        "  GLM_API_KEY       — from https://open.bigmodel.cn (GLM Image)\n"
        "  OPENROUTER_API_KEY — from https://openrouter.ai  (Gemini, GPT-5 Image, etc.)"
    )


# ---------------------------------------------------------------------------
# GLM provider
# ---------------------------------------------------------------------------

def generate_image_glm(
    prompt: str,
    size: str = "1088x1920",
    quality: str = "hd",
    watermark: bool = False,
    output_dir: str = "output",
) -> dict:
    [REDACTED_SECRET])
    url = "https://open.bigmodel.cn/api/paas/v4/images/generations"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}",
    }
    body = {
        "model": "glm-image",
        "prompt": prompt,
        "size": size,
        "quality": quality,
        "watermark_enabled": str(watermark).lower(),
    }

    response = requests.post(url, headers=headers, json=body, timeout=120)
    response.raise_for_status()
    data = response.json()

    if "data" not in data or not data["data"]:
        raise RuntimeError("No image returned from GLM API")

    image_url = data["data"][0]["url"]

    os.makedirs(output_dir, exist_ok=True)
    timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    safe_prompt = re.sub(r'[\\/:*?"<>|]', "", prompt).replace(" ", "_")[:30]
    filename = f"{timestamp}_{safe_prompt}.png"
    filepath = os.path.join(output_dir, filename)

    img_response = requests.get(image_url, timeout=60)
    img_response.raise_for_status()
    with open(filepath, "wb") as f:
        f.write(img_response.content)

    return {"url": image_url, "local_path": filepath, "prompt": prompt, "quality": quality}


# ---------------------------------------------------------------------------
# OpenRouter provider
# ---------------------------------------------------------------------------

def generate_image_openrouter(
    prompt: str,
    model: str = OPENROUTER_DEFAULT_MODEL,
    output_dir: str = "output",
) -> dict:
    [REDACTED_SECRET])
    url = "https://openrouter.ai/api/v1/chat/completions"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    # Some models output both text+image, others image-only.
    # "image" modality is always required; include "text" for hybrid models.
    #
    # Margin guard: gpt-5-image-mini and similar models often place content at
    # the very edge of the canvas, causing visible clipping. Appending an explicit
    # instruction to leave inset padding reliably prevents this.
    MARGIN_GUARD = (
        " LAYOUT: Leave comfortable margins on all four sides so nothing is cut off or "
        "near the edge. Prefer a tall vertical layout for long content. The model may "
        "choose appropriate dimensions — vertical orientation is recommended but not mandatory."
    )
    padded_prompt = prompt + MARGIN_GUARD

    body = {
        "model": model,
        "messages": [{"role": "user", "content": padded_prompt}],
        "modalities": ["image", "text"],
        "stream": False,
    }

    response = requests.post(url, headers=headers, json=body, timeout=120)
    response.raise_for_status()
    data = response.json()

    choices = data.get("choices", [])
    if not choices:
        raise RuntimeError("No choices returned from OpenRouter")

    message = choices[0].get("message", {})
    images = message.get("images")
    if not images:
        raise RuntimeError(
            f"No image in OpenRouter response. Model '{model}' may not support image output.\n"
            f"Try: google/gemini-3-pro-image-preview or openai/gpt-5-image-mini"
        )

    generation_id = data.get("id")
    # Cost is available directly in usage.cost (USD)
    usage_cost = data.get("usage", {}).get("cost")

    image_data_url = images[0]["image_url"]["url"]  # "data:image/png;base64,..."

    os.makedirs(output_dir, exist_ok=True)
    timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    safe_prompt = re.sub(r'[\\/:*?"<>|]', "", prompt).replace(" ", "_")[:30]
    filename = f"{timestamp}_{safe_prompt}.png"
    filepath = os.path.join(output_dir, filename)

    _, b64_data = image_data_url.split(",", 1)
    with open(filepath, "wb") as f:
        f.write(base64.b64decode(b64_data))

    return {
        "url": image_data_url,
        "local_path": filepath,
        "prompt": prompt,
        "generation_id": generation_id,
        "usage_cost": usage_cost,
    }


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(description="Generate image from prompt")
    parser.add_argument("prompt", help="Image description")
    parser.add_argument(
        "--language",
        required=True,
        choices=list(SUPPORTED_LANGUAGES.keys()),
        help=(
            "Prompt language (REQUIRED). Supported: "
            + ", ".join(f"{k} ({v})" for k, v in SUPPORTED_LANGUAGES.items())
        ),
    )
    parser.add_argument(
        "--provider",
        choices=["glm", "openrouter"],
        default=None,
        help="API provider. Auto-detected from available keys if not specified.",
    )
    parser.add_argument(
        "--model",
        default=None,
        help=(
            f"Model to use (OpenRouter only). Default: {OPENROUTER_DEFAULT_MODEL}. "
            "See https://openrouter.ai/collections/image-models"
        ),
    )
    # GLM-only options
    parser.add_argument("--size", default="1088x1920", help="Image size, GLM only (default: 1088x1920)")
    parser.add_argument("--quality", default="hd", help="Image quality, GLM only (default: hd)")
    parser.add_argument("--output", default="output", help="Output directory (default: output)")
    parser.add_argument("--watermark", action="store_true", help="Enable watermark, GLM only")

    args = parser.parse_args()

    provider = args.provider or detect_provider()

    try:
        if provider == "glm":
            if args.model:
                print(f"Warning: --model is ignored for provider 'glm' (model is always glm-image)", file=sys.stderr)
            result = generate_image_glm(
                prompt=args.prompt,
                size=args.size,
                quality=args.quality,
                watermark=args.watermark,
                output_dir=args.output,
            )
        else:  # openrouter
            model = args.model or OPENROUTER_DEFAULT_MODEL
            result = generate_image_openrouter(
                prompt=args.prompt,
                model=model,
                output_dir=args.output,
            )

        lang_label = SUPPORTED_LANGUAGES.get(args.language, args.language)
        print(f"Provider: {provider}")
        print(f"Language: {lang_label} ({args.language})")
        print(f"Image saved: {result['local_path']}")
        print(f"\nMarkdown URL:\n![{result['prompt']}]({result['url']})")
        print(f"\nLocal path: {result['local_path']}")

        # Cost reporting
        if provider == "glm":
            print(f"\nCost: {glm_cost_str(result.get('quality', args.quality))}")
        else:
            cost = result.get("usage_cost")
            if cost is None:
                # fallback: query generation endpoint
                gen_id = result.get("generation_id")
                if gen_id:
                    cost = fetch_openrouter_cost(gen_id, load_openrouter_key())
            if cost is not None:
                print(f"\nCost: ${cost:.6f} USD")
            else:
                print(f"\nCost: unavailable — check https://openrouter.ai/activity")

    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()


path: skill.yml
content: name: glm-image
display_name: "GLM Image Generator"
version: 1.9.0
description: >
  Generate images from text prompts using the GLM-Image API (BigModel / Zhipu AI).
  Use when the user wants to generate, create, or draw an image. Triggers on:
  "generate an image of...", "create a picture of...", "draw...", "make an image of..."
attribution:
  source: https://github.com/ViffyGwaanl/glm-image
  author: ViffyGwaanl
  license: MIT
permissions:
  - exec
  - read
triggers:
  - generate an image
  - create a picture
  - draw a
  - make an image
  - generate image of
  - create image of


Security scan verdicts — ClawScan: suspicious | static analysis:  | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)