# Imgen API v2 — LLM Reference
  
  Concise reference optimized for code-generation agents (Claude Code etc.). For the full human-facing docs see `https://imgen.api.efficientstack.com/docs`.
  
  ## Endpoint
  
  `POST https://imgen.api.efficientstack.com/api/v2`
  
  All requests:
  - Method: `POST`
  - Header: `Content-Type: application/json`
  - Header: `Authorization: Bearer <API-KEY>` (required; 401 if missing/invalid/disabled)
  - Body: JSON with an `action` field
  
  ## Actions
  
  | action | purpose |
  |---|---|
  | `models` | list available characters, models, style presets |
  | `generate` | submit an image generation job, returns a job id |
  | `status` | poll a job until terminal state |
  | `optimize` | LLM-enhance a prompt without generating |
  | `random` | server-side random prompt builder |
  
  ## Prompt Syntax
  
  The `prompt` field is a raw string. The server tokenizes:
  
  - `@name` — activate a character LoRA (one per prompt). Unknown name is ignored.
  - `-term` — comma-separated negative token. Multiple allowed.
  - Everything else — positive prompt text.
  
  Example: `@aya black lingerie, on bed, warm lighting, -red dress, -ugly hands`
  
  ## action: models
  
  Request:
  ```json
  { "action": "models" }
  ```
  
  Response:
  ```json
  {
    "characters": ["aya", "bella", "..."],
    "models": {
      "eros": {
        "label": "Eros",
        "default_style": "professional",
        "styles": { "professional": { "label": "Professional" } }
      }
    }
  }
  ```
  
  ## action: generate
  
  Request fields:
  
  | field | type | required | default | notes |
  |---|---|---|---|---|
  | `action` | string | yes | — | `"generate"` |
  | `prompt` | string | yes | — | raw prompt incl. `@char` / `-neg` |
  | `model` | string | no | first configured | model key from `models` |
  | `negative` | string | no | `""` | extra negatives |
  | `optimize` | bool | no | `false` | LLM-enhance prompt before gen |
  | `use_cache` | bool | no | `false` | try semantic cache first |
  | `cache_only` | bool | no | `false` | 404 on cache miss instead of generating |
  | `cache_force` | bool | no | `false` | ignore 0.95 similarity floor |
  | `cache_pick` | int 1–10 | no | `5` | top-N pool size for cache random pick |
  | `width` | int | no | `1344` | must be a valid pair (see below) |
  | `height` | int | no | `768` | must be a valid pair (see below) |
  | `style` | string | no | model default | style preset key |
  | `format` | string | no | `"jpeg"` | `jpeg` \| `png` \| `webp` \| `avif` |
  | `quality` | int 1–100 | no | `95` | compression quality |
  | `lossless` | bool | no | `false` | webp/avif lossless |
  | `compliance_ruleset` | string | no | `"default"` | ACS ruleset to apply |
  | `precompliance` | string ≤2000 | no | `""` | pre-screening result from upstream keyword filter (Ban Word Service). Forwarded to ACS as supplementary context, NOT authoritative |
  | `wm_image` | string | no | `""` | watermark as base64 data URI |
  | `wm_position` | string | no | `"bottom-right"` | `top-left` \| `top-right` \| `center` \| `bottom-left` \| `bottom-right` |
  | `wm_scale` | int 1–100 | no | `5` | watermark scale |
  | `wm_transparency` | int 0–100 | no | `100` | watermark opacity |
  | `wm_rotation` | int -360–360 | no | `0` | watermark rotation deg |
  | `wm_padding_x` | int 0–500 | no | `10` | watermark x padding |
  | `wm_padding_y` | int 0–500 | no | `10` | watermark y padding |
  | `cs_author` | string | no | `""` | C2PA override |
  | `cs_title` | string | no | `""` | C2PA override |
  | `cs_description` | string | no | `""` | C2PA override |
  | `cs_organization` | string | no | `""` | C2PA override |
  | `cs_vendor` | string | no | `""` | C2PA override |
  
  Valid dimensions (anything else falls back to `1344x768`):
  `1024x1024`, `1344x768`, `768x1344`, `1216x832`, `832x1216`, `1152x896`, `896x1152`
  
  Silent fallbacks: invalid model → first model; invalid style → model default; invalid format → `jpeg`; bad dimensions → `1344x768`; empty/whitespace `compliance_ruleset` → `"default"`; out-of-range `cache_pick` → clamped to `[1,10]`; `precompliance` longer than 2000 chars is truncated.
  
  Response:
  ```json
  {
    "id": "run_abc123",
    "prompt": "@aya enhanced positive text, -red dress",
    "model": "eros",
    "format": "jpeg",
    "gen_id": "a1b2c3d4e5f6g7h8i9j0"
  }
  ```
  
  Cache hit (only when `use_cache: true` and an eligible match exists):
  ```json
  {
    "id": "cache_<hex>",
    "prompt": "...",
    "model": "eros",
    "format": "jpeg",
    "gen_id": "...",
    "cache": true
  }
  ```
  Cache hits expire 1 hour after `generate`. Poll `status` promptly. `cache_only: true` + miss → HTTP 404 `{"error":"No cached result found"}`.
  
  ## action: status
  
  Always pass the original `id`, `prompt`, and `model` from the `generate` response.
  
  ```json
  { "action": "status", "id": "run_abc123", "prompt": "<from generate>", "model": "eros" }
  ```
  
  Possible responses:
  
  Pending:
  ```json
  { "status": "IN_QUEUE" }
  { "status": "IN_PROGRESS" }
  ```
  
  Completed (safe):
  ```json
  {
    "status": "COMPLETED",
    "meta": {
      "id": "...",
      "gen_id": "...",
      "time": "2026-05-28T20:02:50Z",
      "prompt": "...",
      "positive": "...",
      "negative": "...",
      "character": "aya",
      "model": "eros",
      "style": "professional",
      "width": 1344, "height": 768,
      "format": "jpeg", "quality": 95, "lossless": false,
      "optimized": true,
      "compliance": "safe",
      "compliance_codes": [],
      "compliance_ruleset": "default",
      "precompliance": "",
      "fingerprint": "...",
      "images": [
        { "url": "https://...signed.jpg?...", "filename": "...", "type": "signed" },
        { "url": "https://...thumb.jpg?...", "filename": "...", "type": "thumbnail" }
      ],
      "...": "..."
    }
  }
  ```
  
  Completed (cache hit) — identical shape, plus `"cache": true`, always `compliance: "safe"`.
  
  Completed (blocked by ACS):
  ```json
  {
    "status": "COMPLETED",
    "compliance": "unsafe",
    "error": "Content blocked — flagged for: <category names>",
    "filter_categories": ["<acs-rule-id>", "..."],
    "unsafe_url": "<base64-encoded signed S3 URL>"
  }
  ```
  Decode `unsafe_url` with `atob()` (JS) or `base64.b64decode()` (Python) — the asset URL is intentionally base64-wrapped to discourage casual display.
  
  Terminal failure:
  ```json
  { "status": "FAILED" | "TIMED_OUT" | "CANCELLED", "error": "Generation failed" }
  ```
  
  ## action: optimize
  
  ```json
  { "action": "optimize", "prompt": "@aya black lingerie, on bed", "model": "eros" }
  ```
  Returns `{ "prompt": "<enhanced>" }`. Returns the original prompt on a no-op enhancement; `{ "error": "Enhancement failed" }` (502) on upstream LLM error.
  
  ## action: random
  
  ```json
  { "action": "random" }                   // random char (or none if none configured)
  { "action": "random", "character": "" }  // explicitly no character
  { "action": "random", "character": "aya" } // specific character (ignored if unknown)
  ```
  Returns `{ "prompt": "@aya ..." }`.
  
  ## Server-side Prompt Construction
  
  Positive sent to the diffusion model:
  ```
  [trigger], [character_positive], <user prompt>[, <style quality tags>]
  ```
  
  Negative sent to the diffusion model:
  ```
  <base negative>[, <style negative_extra>][, <character negative>][, <parsed -terms>][, <explicit negative>]
  ```
  
  ## Compliance
  
  Every non-cached generation is reviewed by ACS during `status` when the underlying job hits `COMPLETED`. Cached jobs are pre-cleared and skip review.
  
  - `compliance_ruleset` selects which ACS ruleset applies (persisted on the job; not needed on poll).
  - `precompliance` is appended to the ACS context as a clearly labelled supplementary signal. ACS still makes the authoritative call. Use it to forward pre-screening output to give ACS more context without overriding it.
  
  ## Caching (`use_cache: true`)
  
  1. Semantic search the cache with the parsed positive prompt.
  2. Strict equality required on `model`, `style`, `character`.
  3. Default eligibility floor: similarity > 0.95. Disable with `cache_force: true`.
  4. One random pick from top-`cache_pick` (default 5, clamped to `[1, 10]`).
  5. Cache hits bypass `optimize` and ACS review.
  6. Cache miss with `cache_only: false` (default) → falls through to normal generation.
  7. Cache miss with `cache_only: true` → HTTP 404 `{"error":"No cached result found"}`.
  
  ## Polling Strategy (recommended)
  
  - Interval: 3s
  - Max polls: 120 (≈6 min)
  - Wait for each poll to complete before issuing the next (no overlap)
  - Terminal states: `COMPLETED`, `FAILED`, `TIMED_OUT`, `CANCELLED`
  - On `COMPLETED`, always check `compliance` before reading `meta`
  
  ## Errors
  
  | HTTP | error | cause |
  |---|---|---|
  | 400 | `Missing action` | no `action` field |
  | 400 | `Prompt is required` | empty prompt |
  | 400 | `Invalid character` | unknown `@name` referenced in prompt |
  | 400 | `Bad ID` | invalid job id format on `status` |
  | 401 | `Unauthorized` / `Invalid API key` / `API key is disabled` | auth failure |
  | 404 | `No cached result found` | `use_cache` + `cache_only` with no match |
  | 404 | `Cached result not found or expired` | cache job id expired (>1h) |
  | 502 | `Generation failed` | upstream RunPod error |
  | 502 | `Enhancement failed` | upstream LLM error in `optimize` |
  
  ## Minimal JavaScript Client
  
  ```javascript
  const API = 'https://imgen.api.efficientstack.com/api/v2';
  const TOKEN = process.env.IMGEN_API_KEY;
  
  async function api(body) {
    const r = await fetch(API, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TOKEN}` },
      body: JSON.stringify(body),
    });
    return r.json();
  }
  
  async function generate(prompt, opts = {}) {
    const job = await api({
      action: 'generate',
      prompt,
      model: opts.model || 'eros',
      style: opts.style || '',
      use_cache: !!opts.use_cache,
      compliance_ruleset: opts.compliance_ruleset || '',
      precompliance: opts.precompliance || '',
    });
    if (job.error) throw new Error(job.error);
  
    for (let i = 0; i < 120; i++) {
      await new Promise(r => setTimeout(r, 3000));
      const s = await api({ action: 'status', id: job.id, prompt: job.prompt, model: job.model });
      if (s.status === 'IN_QUEUE' || s.status === 'IN_PROGRESS') continue;
      if (s.status === 'COMPLETED') {
        if (s.compliance === 'unsafe') {
          const blockedUrl = s.unsafe_url ? atob(s.unsafe_url) : null;
          const err = new Error(s.error);
          err.unsafeUrl = blockedUrl;
          err.categories = s.filter_categories || [];
          throw err;
        }
        return { meta: s.meta, fromCache: !!s.cache };
      }
      throw new Error(s.error || 'Failed: ' + s.status);
    }
    throw new Error('Polling timed out');
  }
  
  // Example with pre-compliance forwarded from an upstream Ban Word Service:
  const { meta, fromCache } = await generate('@aya black lingerie, warm lighting', {
    style: 'professional',
    use_cache: true,
    precompliance: 'ban-word-service: no banned tokens matched (v3.2)',
  });
  const signed = meta.images.find(i => i.type === 'signed');
  console.log(signed.url, fromCache ? '(cache)' : '(generated)');
  ```
  
  ## Minimal Python Client
  
  ```python
  import os, time, base64, requests
  
  API = "https://imgen.api.efficientstack.com/api/v2"
  TOKEN = os.environ["IMGEN_API_KEY"]
  HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
  
  def call(body):
      return requests.post(API, json=body, headers=HEADERS, timeout=30).json()
  
  def generate(prompt, *, model="eros", style="", use_cache=False, ruleset="", precompliance=""):
      job = call({
          "action": "generate", "prompt": prompt, "model": model, "style": style,
          "use_cache": use_cache, "compliance_ruleset": ruleset, "precompliance": precompliance,
      })
      if "error" in job:
          raise RuntimeError(job["error"])
  
      for _ in range(120):
          time.sleep(3)
          s = call({"action": "status", "id": job["id"], "prompt": job["prompt"], "model": job["model"]})
          if s["status"] in ("IN_QUEUE", "IN_PROGRESS"):
              continue
          if s["status"] == "COMPLETED":
              if s.get("compliance") == "unsafe":
                  blocked = base64.b64decode(s["unsafe_url"]).decode() if s.get("unsafe_url") else None
                  raise RuntimeError(f"{s.get('error')} (blocked_url={blocked})")
              return s["meta"], bool(s.get("cache"))
          raise RuntimeError(s.get("error", f"Failed: {s['status']}"))
      raise TimeoutError("Polling timed out")
  
  meta, from_cache = generate(
      "@aya black lingerie, warm lighting",
      style="professional",
      use_cache=True,
      precompliance="ban-word-service: no banned tokens matched (v3.2)",
  )
  signed = next(i for i in meta["images"] if i["type"] == "signed")
  print(signed["url"], "(cache)" if from_cache else "(generated)")
  ```
  