diff --git a/.gitignore b/.gitignore index 3b9ac34..f7cdeef 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ build/ .env.* .DS_Store .claude/worktrees/ +.waza-results/ +.waza-cache/ diff --git a/.waza.yaml b/.waza.yaml new file mode 100644 index 0000000..7dcaaf8 --- /dev/null +++ b/.waza.yaml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/waza/main/schemas/config.schema.json + +paths: + skills: skills + evals: evals + results: .waza-results +defaults: + engine: copilot-sdk + model: claude-sonnet-4.6 + timeout: 300 + parallel: false + workers: 4 + verbose: false + sessionLog: false +cache: + enabled: false + dir: .waza-cache +server: + port: 3000 + resultsDir: results/ +dev: + model: claude-sonnet-4-20250514 + target: medium-high + maxIterations: 5 +tokens: + warningThreshold: 500 + fallbackLimit: 1000 +graders: + programTimeout: 30 +storage: + containerName: waza-results diff --git a/evals/_hooks/prepare-skill.ts b/evals/_hooks/prepare-skill.ts new file mode 100644 index 0000000..d5e06fd --- /dev/null +++ b/evals/_hooks/prepare-skill.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env bun + +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +// NOTE: Waza runs hooks without a shell, so one TypeScript command handles setup. + +function main(): void { + const skill = process.argv[2]; + if (!skill) { + console.error("usage: prepare-skill.ts "); + process.exit(2); + } + + const here = dirname(fileURLToPath(import.meta.url)); + const repoRoot = resolve(here, "..", ".."); + const srcDir = join(repoRoot, "skills", skill); + if (!existsSync(srcDir)) { + console.error(`❌ skill source not found: ${srcDir}`); + process.exit(1); + } + + const destSkillsRoot = join(homedir(), ".agents", "skills"); + const destDir = join(destSkillsRoot, skill); + + const mk = spawnSync("mkdir", ["-p", destSkillsRoot], { stdio: "inherit" }); + if (mk.status !== 0) { + process.exit(mk.status ?? 1); + } + + const sync = spawnSync("rsync", ["-a", "--delete", `${srcDir}/`, `${destDir}/`], { + stdio: "inherit", + }); + if (sync.status !== 0) { + process.exit(sync.status ?? 1); + } + + const scriptsPkg = join(destDir, "scripts", "package.json"); + if (existsSync(scriptsPkg)) { + const install = spawnSync("npm", ["install", "--silent"], { + cwd: join(destDir, "scripts"), + stdio: "inherit", + }); + if (install.status !== 0) { + process.exit(install.status ?? 1); + } + } + + console.error(`✓ prepared skill "${skill}" → ${destDir}`); +} + +main(); diff --git a/evals/openrouter-images/eval.yaml b/evals/openrouter-images/eval.yaml new file mode 100644 index 0000000..090eff8 --- /dev/null +++ b/evals/openrouter-images/eval.yaml @@ -0,0 +1,32 @@ +name: openrouter-images-eval +description: | + Evaluates routing among image discovery, generation, and editing; indirect + triggers and anti-triggers; and migration to POST /api/v1/images with + data[].b64_json responses. +skill: openrouter-images +version: "1.0" +config: + trials_per_task: 1 + timeout_seconds: 300 + parallel: false + executor: copilot-sdk + model: claude-opus-4.7 +metrics: + - name: task_completion + weight: 1.0 + threshold: 0.8 + description: Did the agent choose the correct workflow and dedicated API shape? + +hooks: + before_run: + - command: "bun evals/_hooks/prepare-skill.ts openrouter-images" + +graders: + - type: code + name: has_output + config: + assertions: + - "len(output) > 50" + +tasks: + - "tasks/*.yaml" diff --git a/evals/openrouter-images/tasks/01-generate-basic.yaml b/evals/openrouter-images/tasks/01-generate-basic.yaml new file mode 100644 index 0000000..e8c2bbb --- /dev/null +++ b/evals/openrouter-images/tasks/01-generate-basic.yaml @@ -0,0 +1,30 @@ +id: generate-basic-001 +name: Generate Basic Image +description: | + A new-image request should route to generate.ts on the dedicated Images API, + not edit.ts or a raw legacy endpoint. +tags: + - happy-path + - generate + +inputs: + prompt: | + Generate an image of a red panda wearing sunglasses. Do not execute the + request because my API key is unavailable; show the exact bundled-script + command you would run. + +graders: + - type: code + name: uses_generate_script + config: + language: python + assertions: + - '"generate.ts" in output' + - '"edit.ts" not in output' + - '"red panda" in output.lower() and "sunglasses" in output.lower()' + - '"/api/v1/chat/completions" not in output and "/api/v1/responses" not in output' + + +expected: + outcomes: + - type: task_completed diff --git a/evals/openrouter-images/tasks/02-generate-with-aspect-ratio.yaml b/evals/openrouter-images/tasks/02-generate-with-aspect-ratio.yaml new file mode 100644 index 0000000..09fb36e --- /dev/null +++ b/evals/openrouter-images/tasks/02-generate-with-aspect-ratio.yaml @@ -0,0 +1,30 @@ +id: generate-aspect-ratio-001 +name: Generate With Aspect Ratio +description: | + A specified landscape ratio should be passed through the bundled generate + script rather than only mentioned in prose. +tags: + - happy-path + - generate + - aspect-ratio + +inputs: + prompt: | + Make a wide landscape image of a futuristic city at night, 16:9. Do not + execute it; show the exact bundled-script command. + +graders: + - type: code + name: aspect_ratio_flag + config: + language: python + assertions: + - '"generate.ts" in output' + - '"--aspect-ratio" in output' + - '"16:9" in output' + - '"city" in output.lower() and "night" in output.lower()' + + +expected: + outcomes: + - type: task_completed diff --git a/evals/openrouter-images/tasks/03-edit-image.yaml b/evals/openrouter-images/tasks/03-edit-image.yaml new file mode 100644 index 0000000..849cb4a --- /dev/null +++ b/evals/openrouter-images/tasks/03-edit-image.yaml @@ -0,0 +1,38 @@ +id: edit-image-001 +name: Edit Existing Image +description: | + User wants to modify an existing image. The agent should use edit.ts, + pass the source path first, and avoid generate.ts and raw API calls. +tags: + - happy-path + - edit + +inputs: + prompt: | + I have a file called photo.png. Edit it so the sky is purple. Do not + execute the request because my API key is unavailable; show the exact + bundled-script command you would run. + +graders: + - type: code + name: uses_only_edit_script + config: + language: python + assertions: + - '"edit.ts" in output' + - '"generate.ts" not in output' + - '"/api/v1/chat/completions" not in output and "/api/v1/responses" not in output' + + - type: code + name: passes_source_then_prompt + config: + language: python + assertions: + - '"photo.png" in output' + - '"purple" in output.lower() and "sky" in output.lower()' + - 'output.find("photo.png") < output.lower().find("purple")' + + +expected: + outcomes: + - type: task_completed diff --git a/evals/openrouter-images/tasks/04-anti-trigger-image-theory.yaml b/evals/openrouter-images/tasks/04-anti-trigger-image-theory.yaml new file mode 100644 index 0000000..adcd412 --- /dev/null +++ b/evals/openrouter-images/tasks/04-anti-trigger-image-theory.yaml @@ -0,0 +1,28 @@ +id: anti-trigger-image-theory-001 +name: Anti-Trigger - Image Generation Theory +description: | + A conceptual question should receive an explanation without invoking or + proposing image-generation scripts. +tags: + - anti-trigger + - negative-test + +inputs: + prompt: | + How do diffusion models generate images from text? Explain the basic idea. + I am curious about the technique, not looking to generate anything. + +graders: + - type: code + name: no_image_scripts + config: + language: python + assertions: + - '"generate.ts" not in output' + - '"edit.ts" not in output' + - '"discover.ts" not in output' + + +expected: + outcomes: + - type: task_completed diff --git a/evals/openrouter-images/tasks/05-indirect-trigger-blog-hero.yaml b/evals/openrouter-images/tasks/05-indirect-trigger-blog-hero.yaml new file mode 100644 index 0000000..8658e92 --- /dev/null +++ b/evals/openrouter-images/tasks/05-indirect-trigger-blog-hero.yaml @@ -0,0 +1,30 @@ +id: indirect-trigger-blog-hero-001 +name: Indirect Trigger - Blog Hero Image +description: | + A request for blog artwork should route to image generation even without the + phrase "generate an image", with an appropriate wide ratio. +tags: + - happy-path + - indirect-trigger + +inputs: + prompt: | + I am writing a blog post about remote work and need a hero image for the + top of the page: a quiet home office with good natural light. Do not execute + it; show the exact bundled-script command you would run. + +graders: + - type: code + name: hero_generation_command + config: + language: python + assertions: + - '"generate.ts" in output' + - '"home office" in output.lower() or "remote work" in output.lower()' + - '"--aspect-ratio" in output' + - '("16:9" in output or "21:9" in output or "3:1" in output or "2:1" in output)' + + +expected: + outcomes: + - type: task_completed diff --git a/evals/openrouter-images/tasks/06-discover-model-capabilities.yaml b/evals/openrouter-images/tasks/06-discover-model-capabilities.yaml new file mode 100644 index 0000000..0510d56 --- /dev/null +++ b/evals/openrouter-images/tasks/06-discover-model-capabilities.yaml @@ -0,0 +1,29 @@ +id: discover-model-capabilities-001 +name: Discover Model Capabilities +description: | + A model capability question should use discover.ts with the model ID before + recommending unsupported generation options. +tags: + - discovery + - model-selection + +inputs: + prompt: | + Before I generate anything, how would you check which parameters and + providers google/gemini-3.1-flash-image-preview supports? Show the exact + bundled command, but do not execute it. + +graders: + - type: code + name: uses_discovery_script + config: + language: python + assertions: + - '"discover.ts" in output' + - '"google/gemini-3.1-flash-image-preview" in output' + - '"generate.ts" not in output and "edit.ts" not in output' + + +expected: + outcomes: + - type: task_completed diff --git a/evals/openrouter-images/tasks/07-dedicated-api-code.yaml b/evals/openrouter-images/tasks/07-dedicated-api-code.yaml new file mode 100644 index 0000000..7ff862b --- /dev/null +++ b/evals/openrouter-images/tasks/07-dedicated-api-code.yaml @@ -0,0 +1,32 @@ +id: dedicated-images-api-code-001 +name: Dedicated Images API Code +description: | + A developer asking for direct API code should be taught the new dedicated + endpoint and response shape rather than legacy chat or responses formats. +tags: + - api + - migration + +inputs: + prompt: | + Give me a minimal TypeScript fetch example for generating an image through + OpenRouter's new dedicated images endpoint. Include how to read the returned + base64 image, but do not send a real request. + +graders: + - type: code + name: dedicated_endpoint_shape + config: + language: python + assertions: + - '"/api/v1/images" in output' + - '"POST" in output.upper()' + - '"b64_json" in output' + - '"data" in output' + - '"/api/v1/chat/completions" not in output' + - '"choices" not in output or "not" in output.lower()' + + +expected: + outcomes: + - type: task_completed diff --git a/skills/openrouter-images/README.md b/skills/openrouter-images/README.md index 4a2f607..8592467 100644 --- a/skills/openrouter-images/README.md +++ b/skills/openrouter-images/README.md @@ -12,7 +12,7 @@ gh skill install OpenRouterTeam/skills openrouter-images Works with Claude Code, Cursor, Codex, OpenCode, Gemini CLI, Windsurf, and [many more agents](https://cli.github.com/manual/gh_skill_install). Add `--scope user` to install across every project for your current agent, or `--agent claude-code` to target a specific agent. -For other install methods (Claude Code plugin marketplace, Cursor Rules, etc.) see the [root README](../../README.md#installing). +The repository root README documents additional install methods for Claude Code, Cursor, and other agents. ## Prerequisites diff --git a/skills/openrouter-images/SKILL.md b/skills/openrouter-images/SKILL.md index 52e1c25..7f89706 100644 --- a/skills/openrouter-images/SKILL.md +++ b/skills/openrouter-images/SKILL.md @@ -1,167 +1,35 @@ --- name: openrouter-images -description: Generate images from text prompts and edit existing images using OpenRouter's dedicated Image API. Use when the user asks to create, generate, or make an image, picture, or illustration from a description, or wants to edit, modify, transform, or alter an existing image with a text prompt. +description: Generate and edit images with OpenRouter's dedicated Images API. Use for requests to create images, illustrations, pictures, blog/social artwork, or modify an existing image; also use when selecting an image model or checking image-model capabilities. Do not use for conceptual questions about image-generation theory or for merely analyzing an image. --- # OpenRouter Images -Generate images from text prompts and edit existing images via OpenRouter's dedicated Image API (`POST /api/v1/images`). The skill also discovers which models exist and which parameters each one accepts, so you pick a valid model and options instead of guessing. +**UTILITY SKILL** — invoke the bundled scripts for image operations. They target `POST /api/v1/images` and parse `data[].b64_json`; do not route image work through Chat Completions or Responses. -## Prerequisites +## Triggers -The `OPENROUTER_API_KEY` environment variable must be set. Get a key at https://openrouter.ai/keys +**USE FOR:** “generate an image,” “make an illustration,” “create a hero image,” “edit this photo,” or “which image model supports this option?” -Discovery (`discover.ts`) is public and works without a key; generation and editing require one. +**DO NOT USE FOR:** image-generation theory or analyzing/describing an existing image without modification. -## First-Time Setup +## Route the request -```bash -cd /scripts && npm install -``` - -## Decision Tree - -Pick the right script based on what the user is asking: - -| User wants to... | Script | Example | -|---|---|---| -| See which image models exist and what they support | `discover.ts` | "What image models can I use?" | -| Check the exact params a specific model accepts | `discover.ts ` | "Does seedream support 4K?" | -| Generate an image from a text description | `generate.ts "prompt"` | "Create an image of a sunset over mountains" | -| Generate with specific options | `generate.ts "prompt" --aspect-ratio 16:9` | "Make a wide landscape image of a forest" | -| Generate with a different model | `generate.ts "prompt" --model ` | "Generate using gemini 3.1 flash lite image" | -| Edit or modify an existing image | `edit.ts path "prompt"` | "Make the sky purple in photo.png" | -| Transform an image with instructions | `edit.ts path "prompt"` | "Add a party hat to the animal in this image" | - -## Discover Capabilities First - -Different models accept different parameters. Rather than hardcoding flags and hitting 400s, discover what's available before generating. - -List every image model with a compact capability summary: - -```bash -cd /scripts && npx tsx discover.ts -``` - -Each entry reports the model `id`, `input_modalities` / `output_modalities` (image input means it supports editing / image-to-image), `supports_streaming`, and a `supported_parameters` map — the union of what any endpoint of that model accepts. - -Inspect one model's definitive per-endpoint capabilities: - -```bash -cd /scripts && npx tsx discover.ts bytedance-seed/seedream-4.5 -``` - -This calls `GET /api/v1/images/models/{author}/{slug}/endpoints` and returns, per provider endpoint: - -| Field | Meaning | -|---|---| -| `provider_name` / `provider_slug` | The serving provider. Use `provider_slug` as the key in `--provider-options`. | -| `supported_parameters` | The exact parameters *this* endpoint accepts, with allowed values. | -| `allowed_passthrough_parameters` | Provider-specific keys you can pass under `--provider-options` (e.g. `steps`, `guidance`). | -| `supports_streaming` | Whether this endpoint streams. | -| `pricing` | Per-image / per-token pricing lines. | - -Capability values print as readable strings: an enum shows as `1K | 2K | 4K`, a range as `0–100`, a boolean as `supported`. A parameter that's absent is unsupported — don't send it. - -## Generate Image - -Create a new image from a text prompt: - -```bash -cd /scripts && npx tsx generate.ts "a red panda wearing sunglasses" -cd /scripts && npx tsx generate.ts "a futuristic cityscape at night" --aspect-ratio 16:9 -cd /scripts && npx tsx generate.ts "pixel art of a dragon" --output dragon.png -cd /scripts && npx tsx generate.ts "a watercolor painting" --model google/gemini-3.1-flash-lite-image --resolution 1K -``` - -## Edit Image - -Modify an existing image with a text prompt. The source image is sent as an image-to-image reference (`input_references`), so use a model whose `input_modalities` include `image` — check with `discover.ts `. - -```bash -cd /scripts && npx tsx edit.ts photo.png "make the sky purple" -cd /scripts && npx tsx edit.ts avatar.jpg "add a party hat" --output avatar-hat.png -cd /scripts && npx tsx edit.ts scene.png "convert to watercolor style" --model google/gemini-3.1-flash-lite-image -``` +- Model/capability question → `npx tsx scripts/discover.ts [author/model]` +- New image → `npx tsx scripts/generate.ts "prompt" [flags]` +- Existing-image change → `npx tsx scripts/edit.ts "prompt" [flags]` -Supported input formats: `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif` +Run commands from this skill directory. Install script dependencies once with `cd scripts && npm install`. Discovery is public; generation and editing require `OPENROUTER_API_KEY`. -## Options +Before using a non-default model or specialized option, run `discover.ts `. Only send parameters listed for a serving endpoint. For editing, choose a model whose input modalities include `image`. -Both `generate.ts` and `edit.ts` accept the same flags. Only pass parameters the target model supports — verify with `discover.ts `. +## Examples -| Flag | Description | Default | -|---|---|---| -| `--model ` | OpenRouter model ID | `google/gemini-3.1-flash-image-preview` | -| `--output ` | Output file path | `image-YYYYMMDD-HHmmss.png` | -| `--aspect-ratio ` | Aspect ratio (e.g. `16:9`, `1:1`, `4:3`) | Model default | -| `--resolution ` | Resolution tier (`512`, `1K`, `2K`, `4K`) | Model default | -| `--size ` | Shorthand: a tier (`2K`) or explicit pixels (`2048x2048`) | Model default | -| `--quality ` | `auto`, `low`, `medium`, or `high` | Model default | -| `--output-format ` | `png`, `jpeg`, `webp`, or `svg` (vector models) | Model default | -| `--background ` | `auto`, `transparent`, or `opaque` | Model default | -| `--output-compression ` | Compression 0–100 for webp/jpeg | Model default | -| `--n ` | Number of images to generate (1–10, provider permitting) | 1 | -| `--seed ` | Seed for deterministic generation (where supported) | Random | -| `--provider-options ` | Provider-specific passthrough, keyed by `provider_slug` | None | - -`--provider-options` takes a JSON object keyed by provider slug, using keys from that endpoint's `allowed_passthrough_parameters`: ```bash -cd /scripts && npx tsx generate.ts "a dramatic portrait" \ - --model black-forest-labs/flux.2-pro \ - --provider-options '{"black-forest-labs": {"steps": 40, "guidance": 3}}' -``` - -## Output Format - -### generate.ts - -```json -{ - "model": "google/gemini-3.1-flash-image-preview", - "prompt": "a red panda wearing sunglasses", - "images_saved": ["/absolute/path/to/image-20260305-143022.png"], - "count": 1 -} +npx tsx scripts/generate.ts "quiet home office in natural light" --aspect-ratio 16:9 +npx tsx scripts/edit.ts photo.png "make the sky purple" --output edited.png +npx tsx scripts/discover.ts google/gemini-3.1-flash-image-preview ``` -### edit.ts - -```json -{ - "model": "google/gemini-3.1-flash-image-preview", - "source_image": "photo.png", - "prompt": "make the sky purple", - "images_saved": ["/absolute/path/to/image-20260305-143055.png"], - "count": 1 -} -``` - -The generation cost (USD) is printed to stderr when the API reports it. When `--n` requests multiple images, each is saved with a `-1`, `-2`, … suffix. - -## API Response Shapes - -Generation uses `POST /api/v1/images`. See the [Image Generation guide](https://openrouter.ai/docs/guides/overview/multimodal/image-generation) for full request/response details. - -Images come back base64-encoded in a `data` array. For raster PNG output, `media_type` is omitted; vector outputs (e.g. SVG) include it, and the saved file extension follows it: - -```json -{ - "created": 1748372400, - "data": [{ "b64_json": "" }], - "usage": { "prompt_tokens": 0, "completion_tokens": 4175, "total_tokens": 4175, "cost": 0.04 } -} -``` - -## Using a Different Model - -The default model is `google/gemini-3.1-flash-image-preview` (Nano Banana 2). To use another, pass `--model ` with any image model ID (e.g. `google/gemini-3.1-flash-lite-image`). Run `discover.ts` to browse image models and `discover.ts ` to confirm which parameters and providers it supports before generating. - -## Presenting Results - -- After generating or editing, display the saved image to the user -- Include the model used and, when reported, the generation cost (printed to stderr) -- If multiple images are returned, show all of them -- When the user doesn't specify an output path, tell them where the file was saved -- For edit operations, mention the source image that was modified +Afterward, present every saved image and report its path, model, source image for edits, and API-reported cost. See [references/images-api.md](references/images-api.md) for response shapes, discovery fields, options, and troubleshooting. diff --git a/skills/openrouter-images/references/images-api.md b/skills/openrouter-images/references/images-api.md new file mode 100644 index 0000000..f17c38e --- /dev/null +++ b/skills/openrouter-images/references/images-api.md @@ -0,0 +1,84 @@ +# Images API reference + +## Endpoints + +- Generate or edit: `POST https://openrouter.ai/api/v1/images` +- List image models: `GET https://openrouter.ai/api/v1/images/models` +- Inspect serving endpoints: `GET https://openrouter.ai/api/v1/images/models/{author}/{slug}/endpoints` + +The dedicated endpoint returns images in `data[].b64_json`. Do not expect Chat Completions' `choices[].message.images` or Responses API output items. + +## Discovery fields + +`discover.ts` summarizes model input/output modalities, streaming support, and supported parameters. Per-endpoint discovery additionally reports: + +- `provider_slug`: key used by `--provider-options` +- `supported_parameters`: accepted standard parameters and allowed values +- `allowed_passthrough_parameters`: provider-specific option names +- `pricing`: billable units and USD costs + +An absent parameter is unsupported by that endpoint. + +## Generation and editing + +Generation sends `model`, `prompt`, and only the options supplied by the user. Editing additionally sends the source image as: + +```json +{ + "input_references": [ + { "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } } + ] +} +``` + +Supported edit inputs are PNG, JPEG, WebP, and GIF. Confirm the selected model accepts image input. + +## Options + +| Flag | API field | Notes | +|---|---|---| +| `--aspect-ratio` | `aspect_ratio` | Examples: `1:1`, `16:9` | +| `--resolution` | `resolution` | Model-specific tier such as `1K`, `2K`, `4K` | +| `--size` | `size` | Tier or explicit dimensions, where supported | +| `--quality` | `quality` | `auto`, `low`, `medium`, or `high`, where supported | +| `--output-format` | `output_format` | Commonly PNG, JPEG, WebP, or SVG | +| `--background` | `background` | `auto`, `transparent`, or `opaque` | +| `--output-compression` | `output_compression` | Integer 0–100 for compressed formats | +| `--n` | `n` | Number of images, subject to endpoint limits | +| `--seed` | `seed` | Deterministic seed, where supported | +| `--provider-options` | `provider.options` | JSON keyed by `provider_slug` | + +Example provider options: + +```bash +npx tsx scripts/generate.ts "dramatic portrait" \ + --model black-forest-labs/flux.2-pro \ + --provider-options '{"black-forest-labs":{"steps":40,"guidance":3}}' +``` + +## Response + +```json +{ + "created": 1748372400, + "data": [{ "b64_json": "" }], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 4175, + "total_tokens": 4175, + "cost": 0.04 + } +} +``` + +`media_type` may accompany vector or non-default output. The scripts use it to choose the saved file extension. Multiple images receive numbered suffixes. + +## Troubleshooting + +- 401: verify `OPENROUTER_API_KEY`. +- 402: add credits at https://openrouter.ai/credits. +- 404: verify the model ID or discovery route. +- 429: wait, then retry. +- 400 or unsupported-parameter errors: inspect the model with `discover.ts ` and remove fields absent from the chosen endpoint. + +Full guide: https://openrouter.ai/docs/guides/overview/multimodal/image-generation diff --git a/skills/openrouter-images/scripts/bun.lock b/skills/openrouter-images/scripts/bun.lock new file mode 100644 index 0000000..1ce4dba --- /dev/null +++ b/skills/openrouter-images/scripts/bun.lock @@ -0,0 +1,80 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "openrouter-images-scripts", + "devDependencies": { + "@types/node": "^26.1.1", + "tsx": "^4.0.0", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], + + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": "bin/esbuild" }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/skills/openrouter-images/scripts/discover.ts b/skills/openrouter-images/scripts/discover.ts index 1507854..9786c6b 100644 --- a/skills/openrouter-images/scripts/discover.ts +++ b/skills/openrouter-images/scripts/discover.ts @@ -11,10 +11,6 @@ if (model) { await listModels(); } -/** - * List every image model with a compact capability summary. Use this to pick a - * model and see, at a glance, which parameters it accepts. - */ async function listModels(): Promise { const { data } = await getImageModels(); const models = data ?? []; @@ -30,10 +26,6 @@ async function listModels(): Promise { console.log(JSON.stringify({ count: summary.length, models: summary }, null, 2)); } -/** - * Show the definitive per-endpoint capabilities for one model: the exact - * parameters each provider accepts, its passthrough allowlist, and pricing. - */ async function showEndpoints(modelSlug: string): Promise { const { id, endpoints } = await getImageModelEndpoints(modelSlug); const rows = (endpoints ?? []).map((e: ImageEndpoint) => ({ @@ -48,10 +40,6 @@ async function showEndpoints(modelSlug: string): Promise { console.log(JSON.stringify({ id, endpoints: rows }, null, 2)); } -/** - * Flatten the typed capability descriptors into human-readable strings so the - * output reads like "resolution: 1K | 2K | 4K" instead of nested objects. - */ function describeParameters( parameters: Record | undefined ): Record { diff --git a/skills/openrouter-images/scripts/lib.ts b/skills/openrouter-images/scripts/lib.ts index d779802..3bdd322 100644 --- a/skills/openrouter-images/scripts/lib.ts +++ b/skills/openrouter-images/scripts/lib.ts @@ -59,11 +59,7 @@ function reportHttpError(status: number, statusText: string, body: string): neve process.exit(1); } -/** - * Generate images via the dedicated Image API (`POST /api/v1/images`). This is - * the canonical image path — image generation is no longer routed through chat - * completions. - */ +// NOTE: Image generation uses the dedicated Images API, not Chat Completions. export async function postImageGeneration(apiKey: string, body: unknown): Promise { const res = await fetch(IMAGES_ENDPOINT, { method: "POST", @@ -82,10 +78,6 @@ export async function postImageGeneration(apiKey: string, body: unknown): Promis return res.json() as Promise; } -/** - * List every image model and its capabilities (`GET /api/v1/images/models`). - * Discovery is public and needs no API key. - */ export async function getImageModels(): Promise { const res = await fetch(IMAGE_MODELS_ENDPOINT); if (!res.ok) { @@ -95,10 +87,6 @@ export async function getImageModels(): Promise { return res.json() as Promise; } -/** - * Fetch the definitive per-endpoint capabilities for one model - * (`GET /api/v1/images/models/{author}/{slug}/endpoints`). - */ export async function getImageModelEndpoints(model: string): Promise { const [author, slug] = model.split("/"); if (!author || !slug) { @@ -142,10 +130,6 @@ const MEDIA_TYPE_EXTENSIONS: Record = { "image/svg+xml": ".svg", }; -/** - * Save one base64 image. The extension follows the response `media_type` when - * present (e.g. SVG from vector models), otherwise the requested output path. - */ export function saveImage(b64: string, outputBase: string, mediaType: string | undefined, index: number, total: number): string { const dotIdx = outputBase.lastIndexOf("."); const stem = dotIdx > 0 ? outputBase.slice(0, dotIdx) : outputBase; @@ -157,11 +141,6 @@ export function saveImage(b64: string, outputBase: string, mediaType: string | u return abs; } -/** - * Build the shared image-config fields from parsed CLI args. Only fields the - * caller passed are included, so the model applies its own defaults for the - * rest. Discover a model's accepted values with `discover.ts` first. - */ export function buildImageParams(args: Map): Record { const params: Record = {}; const setString = (flag: string, key: string) => { diff --git a/skills/openrouter-images/scripts/package.json b/skills/openrouter-images/scripts/package.json index 56962cf..5cb544a 100644 --- a/skills/openrouter-images/scripts/package.json +++ b/skills/openrouter-images/scripts/package.json @@ -3,6 +3,7 @@ "type": "module", "private": true, "devDependencies": { + "@types/node": "^26.1.1", "tsx": "^4.0.0" } }