# phproxy An asynchronous proxy for image generation through Google Gemini or OpenRouter. It supports multipart uploads, key rotation, direct long-running responses, and SQLite-backed polling jobs. ## Start locally Requirements: Python 3.12 or newer. ```sh python -m venv .venv . .venv/bin/activate pip install -e . cp .env.example .env # Set ADMIN_SECRET to a unique random value before starting. set -a && . ./.env && set +a uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 1 ``` The service refuses to start without `ADMIN_SECRET`. It creates the SQLite database and data directory at startup. Open `http://127.0.0.1:8000/ui` for basic administration. The first client and provider key can be created through the UI or API. ## Configure a client Create a client as an administrator: ```sh curl -X POST http://127.0.0.1:8000/api/admin/client \ -H "x-admin-secret: $ADMIN_SECRET" \ -F client_id=booth-01 -F name='Booth 01' -F client_secret='choose-a-client-secret' ``` Add an upstream key (the client secret may also add its own keys): ```sh curl -X POST http://127.0.0.1:8000/api/keys \ -H "auth-secret: $ADMIN_SECRET" \ -F client_id=booth-01 -F provider=google -F api_key="$GOOGLE_API_KEY" -F priority=1 ``` `ADMIN_SECRET` and client secrets are stored only as SHA-256 hashes for client authentication. Upstream provider keys must be retained to make requests, so they are stored in SQLite. Protect the database file and its backups as secrets. ## Generate Direct mode holds the connection open until generation completes: ```sh curl -X POST http://127.0.0.1:8000/v1/generate \ -H 'x-client-secret: choose-a-client-secret' \ -F client_id=booth-01 -F model=nano-banana \ -F prompt='Restyle the subject using the second image as a visual reference' \ -F params='{"aspect_ratio":"9:16","image_size":"2K"}' \ -F 'images=@capture.jpg;type=image/jpeg' -F 'images=@style.png;type=image/png' ``` Polling mode accepts the same multipart fields with `?async_mode=true`. It returns a `job_id`; poll the returned URL with `client_id` as a query parameter and the same `x-client-secret` header. Jobs and their results are stored in SQLite so they survive a client disconnect. Jobs still processing when the proxy process restarts are marked failed because the in-process worker cannot resume safely. The built-in `nano-banana` alias defaults to Gemini 2.5 Flash Image. Set `GOOGLE_MODEL_NANO_BANANA` and `OPENROUTER_MODEL_NANO_BANANA` to provider model IDs you have confirmed are available for your account. Other model IDs are passed through (with `google/` prefix normalization for the native provider). ## Configuration | Variable | Default | Purpose | | --- | --- | --- | | `ADMIN_SECRET` | required | Protects administration endpoints. Use a long random value. | | `DB_PATH` | `./data/proxy.db` | SQLite database location. | | `MAX_UPLOAD_BYTES` | `52428800` | Aggregate raw image upload limit. | | `MAX_IMAGES` | `8` | Maximum reference image count. | | `HTTP_TIMEOUT_SECONDS` | `300` | Overall upstream request timeout. | | `CONNECT_TIMEOUT_SECONDS` | `15` | Upstream connect timeout. | | `GOOGLE_MODEL_NANO_BANANA` | `gemini-2.5-flash-image` | Native Google alias target. | | `OPENROUTER_MODEL_NANO_BANANA` | `google/gemini-2.5-flash-image` | OpenRouter alias target. | Only one Uvicorn worker is supported for this initial implementation. SQLite WAL is intended for a local persistent disk, not a shared network filesystem. Configure Nginx body and proxy timeouts to match your deployment limits; see [`deploy/nginx.conf`](deploy/nginx.conf). ## API outline See the copyable endpoint guide in [`docs/API.md`](docs/API.md). - `POST /v1/generate` — multipart `client_id`, `prompt`, `model`, optional `provider`, JSON `params`, repeated `images`; optional `?async_mode=true`. - `GET /v1/jobs/{job_id}?client_id=...` — job status/result, tenant-scoped. - `GET /api/admin/state` — client and redacted key state (`x-admin-secret`). - `POST /api/admin/client` — create/update a client (`x-admin-secret`). - `POST /api/keys` and `DELETE /api/keys/{id}` — manage keys with `auth-secret` (admin or owning client secret). - `GET /healthz` and `GET /ui`. Errors use FastAPI's `{"detail": ...}` envelope. Provider failures return HTTP 502 with provider names and status codes only; upstream response bodies and credentials are not returned. ## Deploy with Docker ```sh docker build -t phproxy . docker run --name phproxy --restart unless-stopped \ -p 127.0.0.1:8000:8000 \ -e ADMIN_SECRET="$ADMIN_SECRET" \ -e DB_PATH=/data/proxy.db \ -v phproxy-data:/data \ phproxy ``` Terminate TLS at a trusted reverse proxy and keep the application port private. The sample Nginx configuration is a location snippet; add your certificate and domain configuration separately.