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. Uploaded images are decoded and re-encoded with Pillow; only JPEG, PNG, and WebP are accepted.
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:
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):
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:
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. |
MAX_IMAGE_PIXELS |
25000000 |
Maximum width × height for each uploaded image. |
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.
API outline
See the copyable endpoint guide in docs/API.md.
POST /v1/generate— multipartclient_id,prompt,model, optionalprovider, JSONparams, repeatedimages; 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/keysandDELETE /api/keys/{id}— manage keys withauth-secret(admin or owning client secret).GET /healthzandGET /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
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.