# AGENTS.md Bun + Hono server that proxies an OpenAI-compatible image endpoint and serves a small vanilla TS playground. SSE end-to-end: streams gpt-image partial previews through, with keepalive comments that survive Cloudflare's 120s proxy-read timeout. ## Runtime - Bun, not Node. See `CLAUDE.md` for the full Bun-vs-Node cheatsheet (prefer `Bun.serve`, `Bun.file`, `bun:test`, `Bun.sql`, etc.). Do not add `dotenv` — Bun loads `.env` automatically. - Bun version baseline: `1.3.13` (per `README.md`). ## Config Required env vars (validated at startup via `requireEnv`; the process exits if any is missing): | Var | Example | Purpose | |---|---|---| | `BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible base URL | | `API_KEY` | `sk-…` | Bearer token sent to upstream | | `MODEL` | `gpt-image-2` | Model name forwarded to upstream | `.env.example` is the source of truth for variable names. The real `.env` is gitignored. Restart the server after changing env vars — they are read once at module load. These secrets stay **server-side**. The browser only sends `prompt`, `size`, and `referenceImages`. Combined with the `0.0.0.0` bind, this means anyone reachable on the network can spend your upstream quota — bind to `127.0.0.1` or put auth in front if that matters. ## Commands - Install: `bun install` - Dev (HMR): `bun run dev` → `bun --hot ./index.ts` - Start: `bun run start` → `bun ./index.ts` - Typecheck: no script defined. Use `bunx tsc --noEmit` (tsconfig already sets `noEmit: true`, so plain `bunx tsc` works too). - Tests / lint / formatter: none configured. If adding tests, use `bun test`. The server binds `0.0.0.0` (see `index.ts`), so it is reachable from other hosts on the network — be mindful when entering API keys. Bun's dev server auto-serves `/.well-known/appspecific/com.chrome.devtools.json` advertising the project root to Chrome DevTools' "Automatic Workspace Folders". Sandboxed browsers (Flatpak/Snap) reject the path with `Unable to add filesystem: `. Disabled via `development.chromeDevToolsAutomaticWorkspaceFolders: false`. ## Architecture Three files do everything: - `index.ts` — Hono app mounted under `Bun.serve` (`fetch: app.fetch`). - `routes: { "/": index }` serves `index.html` via Bun's HTML bundler; everything else falls through to Hono. - `idleTimeout: 255` (max) — `Bun.serve`'s 10s default kills SSE connections before the first keepalive can fire. The symptom is an empty EventStream in DevTools and `request timed out after 10 seconds` in the log. - `POST /api/generate` uses `streamSSE` from `hono/streaming`. Accepts `{ prompt, size, referenceImages? }` — `BASE_URL`, `API_KEY`, and `MODEL` come from env, not the request. Emits: - `event: partial` — `{ image: dataUrl, index }` for each `image_generation.partial_image` / `image_edit.partial_image`. - `event: final` — `{ image: dataUrl }` for `*.completed`. - `event: done` — empty payload, sent before stream ends. - `event: error` — `{ message }` for any failure. - First write is `: connected\n\n` so the browser/EventStream tab becomes responsive immediately; then a `: keepalive\n\n` raw comment every 15s. - Upstream dispatch: - `referenceImages` present → `POST {baseURL}/images/edits` as `multipart/form-data` (blobs decoded from data URLs via `decodeDataUrl` → `Uint8Array`). Single reference uses field name `image`; **two or more references use `image[]`** to match OpenAI's documented array syntax — strict gateways reject repeated `image` parts with a `duplicate_parameter` 400. - Otherwise → `POST {baseURL}/images/generations` as JSON. - Always sends `stream: true, partial_images: 2` first. On a 400 that mentions `stream` or `partial_images` (see `isStreamingUnsupportedError`), retries once with `stream: false` and replays the JSON response as a single `final` event via `forwardUpstreamJSON`. Any other 4xx/5xx becomes an `error` event. - `AbortController` wired to `stream.onAbort()` and threaded as `signal` into every upstream `fetch`. The `catch` branch is suppressed when `signal.aborted` so closed tabs don't spam the log. - Targets the **gpt-image series only** (gpt-image-2 default). Do not reintroduce DALL·E-only fields like `response_format` — gpt-image always returns `b64_json`. - `gpt-image-2` size constraints (per the OpenAI cookbook): both edges multiple of 16, max edge **< 3840**, long/short ≤ 3:1, total pixels in 655,360–8,294,400, `auto` not supported. Exact 16:9 requires `16k × 9k` with `k` a multiple of 16 (so `1280×720`, `1536×864`, `2048×1152`, `2560×1440`, …). Sizes above 2560×1440 are experimental — the popular 4K target `3840×2160` violates the `< 3840` rule, round down to `3824×2144` if you need it. Common misses: `1920×1080` is **not valid** (1080 % 16 ≠ 0). - `client.ts` — browser entry, loaded via `