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.
- The initial service runs as one Uvicorn process against a local SQLite database on persistent disk.
## Requirements
### Functional
1. Accept multipart image-generation requests at `POST /v1/generate`.
2. Support direct long-hold responses and `async_mode=true` polling jobs.
3. Support Google native Gemini and OpenRouter adapters, including multiple uploaded images and normalized image/text outputs.
4. Resolve named model aliases from configuration and pass through explicit model IDs.
5. Store multiple provider keys per client and try keys by priority, preferring the requested provider first.
6. 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.
7. Persist clients, provider keys, jobs, and job results in SQLite WAL mode.
8. Provide a minimal browser UI and admin/client key-management API.
9. Return upstream-reported usage and per-generation USD cost when the provider supplies it.
10. 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.
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.
`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`.
-`provider_keys`: owned by a client with cascading deletion; provider constrained to `google`/`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
1. FastAPI validates the multipart request and client authentication.
6. 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.
- [x] Add local startup documentation and Docker/Nginx deployment files.
- [x] 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.