# Image generation (HTTP API)

All requests: header `X-Model-Key: $PHOENIX_MODEL_KEY` (not `Authorization: Bearer` — that is for the LLM API). Base: `https://ai.suzatei.com`.

## Python: submit → poll → download

```python
import os, time, requests

BASE = os.environ.get("PHOENIX_BASE_URL", "https://ai.suzatei.com").rstrip("/")
H = {"X-Model-Key": os.environ["PHOENIX_MODEL_KEY"]}

# 1. Pick a model id from the live catalog (never hardcode)
catalog = requests.get(f"{BASE}/workspace/api/generate/models", headers=H, timeout=30).json()
modes = [m for s in catalog["series"] for m in s["modes"]]
model = next(m for m in modes if m["group"] == "image" and not m["refs"])["model"]   # a text-to-image mode

# 2. Submit (do not auto-retry this POST)
r = requests.post(f"{BASE}/workspace/api/generate/submit", headers=H, timeout=60,
                  json={"kind": "image", "model": model, "prompt": "a phoenix rising from fire", "params": {}})
r.raise_for_status()
gid = r.json()["generation"]["id"]

# 3. Poll every 3s, give up after 180s
deadline = time.time() + 180
while time.time() < deadline:
    g = requests.get(f"{BASE}/workspace/api/generate/record/{gid}", headers=H, timeout=30).json()["generation"]
    if g["status"] == "completed":
        break
    if g["status"] == "failed":
        raise RuntimeError(g.get("error") or "generation failed")
    time.sleep(3)
else:
    raise TimeoutError(f"still running, generation id: {gid}")

# 4. Results are site-relative paths; the file URL carries its own token
for i, path in enumerate(g["results"]):
    r = requests.get(BASE + path, timeout=120)
    ext = "png" if "png" in r.headers.get("content-type", "") else "jpg"   # 输出格式随模型参数而定
    open(f"out-{i}.{ext}", "wb").write(r.content)
```

## Node.js (fetch)

```js
const BASE = process.env.PHOENIX_BASE_URL ?? "https://ai.suzatei.com";
const H = { "X-Model-Key": process.env.PHOENIX_MODEL_KEY, "Content-Type": "application/json" };

const submit = await fetch(`${BASE}/workspace/api/generate/submit`, {
  method: "POST", headers: H,
  body: JSON.stringify({ kind: "image", model: MODEL_FROM_CATALOG, prompt: "a phoenix", params: {} }),
});
const { generation } = await submit.json();

for (let i = 0; i < 60; i++) {
  await new Promise((r) => setTimeout(r, 3000));
  const { generation: g } = await (await fetch(`${BASE}/workspace/api/generate/record/${generation.id}`, { headers: H })).json();
  if (g.status === "completed") { console.log(g.results.map((p) => BASE + p)); break; }
  if (g.status === "failed") throw new Error(g.error || "failed");
}
```

## Reference images (edit modes)

A catalog mode with a non-empty `refs` array needs uploads first. Each `refs` item has `field`, `kind`, `max`, `required`.

```python
with open("input.png", "rb") as f:
    up = requests.post(f"{BASE}/workspace/api/generate/upload", params={"kind": "image", "name": "input.png"},
                       headers=H, data=f.read(), timeout=120).json()
att_id = up["attachment"]["id"]

requests.post(f"{BASE}/workspace/api/generate/submit", headers=H, json={
    "kind": "image", "model": EDIT_MODEL, "prompt": "make it night",
    "params": {}, "refs": {"images": [att_id]},        # key = the mode's refs[].field
})
```

Upload limits: image 15 MB, video 60 MB, audio 30 MB; 30 uploads/min and 50/day by default. The file content must match `kind` (checked by magic bytes).
