modelbeast/server/operators/openrouter_image/run.py
MODELBEAST 19f086773e Headless control: mb CLI, AGENTS.md handover, serve scripts; verified-current image models
- mb: zero-dependency python CLI for the full API (ops/upload/run/wait/follow/
  download incl. folder assets, retry/cancel, settings). Schema-aware -p k=v
  coercion. Works from any tailnet machine via MB_HOST. Tested end-to-end
  (upload -> blender_convert -> download).
- AGENTS.md: complete handover brief for other agents using this box as an
  asset factory — endpoints, CLI reference, catalog, recipes, lanes/etiquette.
- scripts/serve.sh (headless start/restart) + scripts/install_launchagent.sh
  (optional boot persistence, owner-run)
- flux_local upgraded to mflux 0.18 reality: flux2-klein-4b default (Apache,
  UNGATED — runs with zero keys), klein-9b, schnell/schnell-4bit community
  quant, dev/krea-dev. Research verdict: FLUX.1-dev no longer competitive
  (Elo ~1027) vs klein ~1083-1119 vs nano-banana ~1154.
- openrouter_image rewritten to the dedicated Image API (POST /api/v1/images):
  b64_json parsing, seed, resolution/aspect, exact usage.cost logging; model
  enum: gemini-2.5-flash-image / 3.1-flash-image (NB2) / 3-pro-image

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:38:52 +10:00

77 lines
2.4 KiB
Python

import argparse
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
ap = argparse.ArgumentParser()
ap.add_argument("--input", action="append", default=[])
ap.add_argument("--outdir", required=True)
ap.add_argument("--params", default="{}")
a = ap.parse_args()
p = json.loads(a.params)
if not p.get("prompt"):
print("ERROR: prompt is required")
sys.exit(1)
key = os.environ.get("OPENROUTER_API_KEY")
if not key:
print("ERROR: OPENROUTER_API_KEY not set — add it in Settings")
sys.exit(1)
model = p.get("model", "google/gemini-2.5-flash-image")
body = {
"model": model,
"prompt": p["prompt"],
"resolution": p.get("resolution", "1K"),
"aspect_ratio": p.get("aspect_ratio", "1:1"),
"n": int(p.get("n", 1)),
}
if p.get("seed") not in (None, ""):
body["seed"] = int(p["seed"])
req = urllib.request.Request(
"https://openrouter.ai/api/v1/images",
data=json.dumps(body).encode(),
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json",
"X-OpenRouter-Title": "MODELBEAST"},
method="POST",
)
print(f"calling {model} via OpenRouter Image API ...", flush=True)
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=300) as resp:
result = json.loads(resp.read())
except urllib.error.HTTPError as e:
print(f"ERROR {e.code}: {e.read().decode()[:1500]}")
sys.exit(1)
elapsed = round(time.time() - t0, 1)
outdir = Path(a.outdir)
outputs = []
for i, item in enumerate(result.get("data", [])):
b64 = item.get("b64_json")
if not b64:
continue
media = item.get("media_type", "image/png")
ext = ".png" if "png" in media else ".webp" if "webp" in media else ".jpg"
short = model.split("/")[-1]
name = f"{short}_{i}{ext}" if len(result.get("data", [])) > 1 else f"{short}{ext}"
(outdir / name).write_bytes(base64.b64decode(b64))
outputs.append({"path": name, "meta": {"tool": "openrouter", "model": model,
"seconds": elapsed,
"cost_usd": result.get("usage", {}).get("cost")}})
if not outputs:
print("ERROR: no image in response:")
print(json.dumps(result, indent=2)[:1500])
sys.exit(1)
usage = result.get("usage", {})
print(f"done in {elapsed}s — {len(outputs)} image(s), cost ${usage.get('cost', '?')}", flush=True)
(outdir / "result.json").write_text(json.dumps({"outputs": outputs}))