7.7 KiB
Project Design Requirements: phproxy
Status: initial implementation
Purpose
Provide a small single-host service that routes image-generation requests from installation clients to configured Google Gemini or OpenRouter API keys. It must tolerate long upstream response times, multiple reference images, client disconnects in polling mode, and upstream key/provider failures.
Users and operating model
- Photobooth/installation clients submit a prompt, model, optional parameters, and zero or more images.
- An administrator provisions clients and manages access.
- Every client must have a secret. Generation, job polling, and client-managed key operations require that secret.
- The initial service runs as one Uvicorn process against a local SQLite database on persistent disk.
Requirements
Functional
- Accept multipart image-generation requests at
POST /v1/generate. - Support direct long-hold responses and
async_mode=truepolling jobs. - Support Google native Gemini and OpenRouter adapters, including multiple uploaded images and normalized image/text outputs.
- Resolve named model aliases from configuration and pass through explicit model IDs.
- Store multiple provider keys per client and try keys by priority, preferring the requested provider first.
- Retry another configured key/provider for rate-limit, server, and connection/timeout failures. Do not retry malformed or rejected requests as if a provider were unavailable.
- Persist clients, provider keys, jobs, and job results in SQLite WAL mode.
- Provide a minimal browser UI and admin/client key-management API.
- Return upstream-reported usage and per-generation USD cost when the provider supplies it.
- Record request intake, upstream, and total execution time, provider/model selection, failures, image count/bytes, and reported cost. Show recent records and aggregates in the admin UI.
Non-functional and security
- Use async HTTP for all upstream network I/O and a shared bounded connection pool.
- Bound aggregate uploaded bytes, image count, and decoded pixel dimensions. Accept only valid JPEG, PNG, and WebP images; re-encode before forwarding to strip metadata and trailing payloads.
- Require an explicit administrator secret at startup; never ship a default secret.
- Store client secrets hashed. Store provider keys only because they are needed for upstream requests; restrict database file access and redact keys from API responses.
- Scope job reads and client key operations to the authenticated owning client.
- Never return upstream response bodies or internal exception traces to callers.
- Keep polling job ownership in the database. A process restart must not leave a job forever marked as running.
- Use a single app worker with local SQLite in this release. Horizontal scaling and durable background execution are out of scope.
API contracts
Generate
POST /v1/generate multipart fields:
client_id(required)prompt(optional, defaults empty)model(optional, defaultsnano-banana)provider(optional:googleoropenrouter)params(optional JSON object string)images(repeated image upload fields)x-client-secretheader (required)
Success is 200 with provider_used, model_used, images, text, elapsed time, and failover attempt count. With ?async_mode=true, success is 200 with job_id, status, and poll URL.
Poll
GET /v1/jobs/{job_id}?client_id={client_id}&include_result=false plus the client's secret header. Returns small status metadata and, when complete, result metadata with binary image download links. The links are signed, scoped to one image, and expire after one hour by default, so they can be opened directly in a browser without sending a client secret. Treat them as temporary bearer credentials. Authenticated clients may also use GET /v1/jobs/{job_id}/images/{image_index}?client_id={client_id} with the secret header. include_result=true retains the legacy Base64 response. Terminal failures include a sanitized error. Unknown and cross-client job IDs return 404.
Management
- Admin routes use
x-admin-secret. - Key create/delete uses
auth-secret, accepted only for the administrator or the owning client. - Admin state returns only a short key prefix and never a full key.
Validation errors use FastAPI's detail format; upstream failures return 502 with sanitized attempt metadata.
Data model
clients: client ID primary key, name, required client-secret hash, activation flag, creation timestamp.provider_keys: owned by a client with cascading deletion; provider constrained togoogle/openrouter; key, priority, activation and failure metadata.jobs: owned by a client with cascading deletion; status, serialized request/result/error and timestamps. On startup, interrupted jobs are marked failed.
SQLite connections enable foreign keys and busy timeout; WAL is initialized once the DB opens. Provider-route and job-owner lookups are indexed. The DB and backups contain upstream credentials and potentially image data in queued job payloads/results, so filesystem and backup permissions are part of deployment security.
Architecture and flow
- FastAPI validates the multipart request and client authentication.
- Uploaded images are bounded, decoded, re-encoded to an allowlisted raster format, then encoded for the provider's JSON API.
- A route loads the active client's provider keys from SQLite.
- The adapter translates the request to Google or OpenRouter format and sends through shared async
httpx. - A retryable upstream failure advances to the next configured key; success is normalized into a provider-neutral response.
- Polling mode persists a queued job before scheduling the single-process async worker. Compact status and result metadata live in SQLite; generated async images are converted to WebP and written to a private output directory. Image files are deleted after 24 hours while job, timing, and cost metadata remain. Prompt and input-image payloads are cleared from the job record at completion or failure.
Operational plan
- Install Python dependencies and set a unique
ADMIN_SECRET. - Mount persistent local storage for both the SQLite database and generated output directory. Keep database and output backups access-restricted.
- Run one Uvicorn worker as
www-data, bound to127.0.0.1:8081. Put TLS and request-size enforcement at Nginx; keep the application port private. - Create clients, add provider credentials, then call
/healthzand verify one small generation using the intended account/model. - Back up the database securely. Treat backups and job records as secrets and apply retention appropriate to the installation.
Delivery plan
- Define requirements, API, data model, constraints, and operational plan.
- Add runnable FastAPI service, SQLite persistence, adapters, and admin UI/API.
- Add local startup documentation and Docker/Nginx deployment files.
- Add per-generation execution and cost history to the admin API/UI.
- Run deployment smoke checks with actual provider credentials and confirm account/model availability before production.
Out of scope / known limits
- Jobs are persisted but are executed in-process; a service restart marks active jobs failed rather than resuming them.
- SQLite WAL assumes local storage and one app process. Multi-host operation needs a different DB and durable queue.
- Upstream model availability and provider-specific parameter support vary by account and change over time. Configure and validate model IDs per provider.
- The initial admin portal has no user/session system; protect it behind network access controls and HTTPS.
- Memory use grows with uploaded image size and provider response size while a generation runs; completed async image payloads are stored as files rather than Base64 in SQLite.