180 lines
9.3 KiB
Markdown
180 lines
9.3 KiB
Markdown
# phproxy API
|
||
|
||
Production base URL: `https://ph_proxy.sokaris.link` (local development uses `http://127.0.0.1:8000`).
|
||
|
||
The service accepts `multipart/form-data` for generation. Management routes also accept form data. Responses are JSON unless noted. The examples use `curl`; an agent can translate them to Python `requests` or `httpx`.
|
||
|
||
## Credentials and first-time setup
|
||
|
||
There are two kinds of credentials:
|
||
|
||
- **Admin secret**: configured by the service operator. Send it as `x-admin-secret` to administrator endpoints, or `auth-secret` to provider-key endpoints.
|
||
- **Client secret**: configured when creating a client. Send it as `x-client-secret` when generating or polling. It can also be used as `auth-secret` for that client's provider-key endpoints.
|
||
|
||
Set `ADMIN_SECRET` in the service environment. Do not commit it or include it in client scripts.
|
||
|
||
Create a client:
|
||
|
||
```sh
|
||
BASE_URL=https://ph_proxy.sokaris.link
|
||
ADMIN_SECRET='value-from-the-service-environment'
|
||
|
||
curl -sS -X POST "$BASE_URL/api/admin/client" \
|
||
-H "x-admin-secret: $ADMIN_SECRET" \
|
||
-F 'client_id=booth-01' \
|
||
-F 'name=Booth 01' \
|
||
-F 'client_secret=replace-with-a-client-secret'
|
||
```
|
||
|
||
Response:
|
||
|
||
```json
|
||
{"status":"ok","client_id":"booth-01"}
|
||
```
|
||
|
||
Add an upstream API key. Use `google` or `openrouter`; lower `priority` values are tried first within the selected provider ordering.
|
||
|
||
```sh
|
||
curl -sS -X POST "$BASE_URL/api/keys" \
|
||
-H "auth-secret: $ADMIN_SECRET" \
|
||
-F 'client_id=booth-01' \
|
||
-F 'provider=google' \
|
||
-F 'api_key=YOUR_GOOGLE_API_KEY' \
|
||
-F 'priority=1'
|
||
```
|
||
|
||
The same endpoint may be authorized with the owning client's secret instead of the admin secret. Add more provider keys at priorities 2, 3, and so on for fallback.
|
||
|
||
## Generate synchronously
|
||
|
||
`POST /v1/generate` holds the HTTP connection until the provider returns. Form fields:
|
||
|
||
| Field | Required | Meaning |
|
||
| --- | --- | --- |
|
||
| `client_id` | Yes | Provisioned client identifier. |
|
||
| `prompt` | No | Text prompt. Defaults to an empty string. |
|
||
| `model` | No | Provider model ID or alias. Defaults to `nano-banana`. |
|
||
| `provider` | No | Preferred provider: `google` or `openrouter`. If omitted, configured key order is used. |
|
||
| `params` | No | JSON object encoded as a string. Defaults to `{}`. |
|
||
| `images` | No | Repeat this multipart field for each reference image. |
|
||
|
||
`x-client-secret` is required for every generation and job-poll request. Clients without a configured secret are rejected. Uploads accept JPEG, PNG, or WebP only (not GIF or SVG), up to 8 files and 50 MiB aggregate by default. The service decodes and re-encodes each image before forwarding it, stripping metadata and non-image payloads, and rejects malformed images, mismatched MIME types, and images larger than 25 megapixels by default. Operators can tune the limits.
|
||
|
||
```sh
|
||
curl -sS -X POST "$BASE_URL/v1/generate" \
|
||
-H 'x-client-secret: replace-with-a-client-secret' \
|
||
-F 'client_id=booth-01' \
|
||
-F 'prompt=Restyle the person using the second image as a style reference' \
|
||
-F 'model=nano-banana' \
|
||
-F 'provider=google' \
|
||
-F 'params={"aspect_ratio":"9:16","image_size":"2K"}' \
|
||
-F 'images=@capture.jpg;type=image/jpeg' \
|
||
-F 'images=@style.png;type=image/png'
|
||
```
|
||
|
||
Success (`200`):
|
||
|
||
```json
|
||
{
|
||
"provider_used": "openrouter",
|
||
"model_used": "google/gemini-2.5-flash-image",
|
||
"images": [{"mime_type": "image/png", "b64_json": "..."}],
|
||
"text": "",
|
||
"cost_usd": 0.0412,
|
||
"cost_source": "openrouter_usage",
|
||
"usage": {"prompt_tokens": 1200, "completion_tokens": 1290, "cost": 0.0412},
|
||
"elapsed_sec": 12.34,
|
||
"failover_attempts": 0
|
||
}
|
||
```
|
||
|
||
`cost_usd` is the amount reported by OpenRouter for that generation. The proxy requests OpenRouter usage metadata and returns it under `usage`; it also includes OpenRouter's request ID and routing/timing metadata when available. For providers that do not return a billed amount (currently Google native), `cost_usd` is `null` and `cost_source` is `unavailable`. Completed async jobs expose these fields inside `result`.
|
||
|
||
Generation responses also include `generation_id`, `request_intake_sec`, and `total_sec`. `request_intake_sec` covers the request reaching and being parsed by the proxy before the generation is queued; `elapsed_sec` is the upstream call time; `total_sec` runs from request receipt through completion. These measurements help separate slow client uploads from slow model calls.
|
||
|
||
An image can be returned as `b64_json`, or as a URL when the upstream returns a hosted image. Decode `b64_json` as standard Base64 before saving it. Avoid printing or logging generated image data in an agent script.
|
||
|
||
## Generate asynchronously and poll
|
||
|
||
Add the query parameter `async_mode=true` to the same multipart request. The response returns as soon as the job is queued:
|
||
|
||
```sh
|
||
curl -sS -X POST "$BASE_URL/v1/generate?async_mode=true" \
|
||
-H 'x-client-secret: replace-with-a-client-secret' \
|
||
-F 'client_id=booth-01' \
|
||
-F 'prompt=Make a cinematic portrait' \
|
||
-F 'model=nano-banana' \
|
||
-F 'images=@capture.jpg;type=image/jpeg'
|
||
```
|
||
|
||
Queued response (`200`):
|
||
|
||
```json
|
||
{"job_id":"<uuid>","status":"queued","poll_url":"/v1/jobs/<uuid>"}
|
||
```
|
||
|
||
Poll every couple of seconds. Include both the `client_id` query parameter and `x-client-secret` header:
|
||
|
||
```sh
|
||
curl -sS "$BASE_URL/v1/jobs/<uuid>?client_id=booth-01" \
|
||
-H 'x-client-secret: replace-with-a-client-secret'
|
||
```
|
||
|
||
While running:
|
||
|
||
```json
|
||
{"job_id":"<uuid>","status":"processing","created_at":1234567890.0,"updated_at":1234567891.0}
|
||
```
|
||
|
||
On success, `status` is `completed` and a `result` field contains the synchronous response object. On failure, `status` is `failed` and `error` contains a sanitized message. Jobs are scoped to the owning `client_id`; an unknown job ID and a job belonging to another client both return `404`.
|
||
|
||
## Models and parameters
|
||
|
||
The built-in `nano-banana` alias maps to `gemini-2.5-flash-image` for Google and `google/gemini-2.5-flash-image` for OpenRouter by default. Operators can override these IDs with `GOOGLE_MODEL_NANO_BANANA` and `OPENROUTER_MODEL_NANO_BANANA`. Other model IDs pass through, with provider prefix normalization.
|
||
|
||
Recognized `params` include:
|
||
|
||
- `aspect_ratio`, `image_size`
|
||
- Google: `temperature`, `topP`, `seed`, `responseModalities`, `safetySettings`, `thinking`, `generationConfig`
|
||
- OpenRouter: `temperature`, `seed`, `reasoning` (object), `modalities`, `extra_body`
|
||
|
||
For OpenRouter, `reasoning` must be an object (for example `{"effort":"medium"}`); boolean values are omitted because the upstream API expects an object. Reasoning controls apply only to models that support them.
|
||
|
||
Provider APIs may reject unsupported parameters or model IDs. Configure exact IDs supported by the provider account. For a requested provider, fallback tries other active keys in configured priority order after a rate limit, server error, or connection/timeout failure. A non-retryable provider response stops the attempt sequence.
|
||
|
||
## Management endpoints
|
||
|
||
| Method and path | Auth | Behavior |
|
||
| --- | --- | --- |
|
||
| `GET /healthz` | None | Liveness response `{"status":"ok"}`. |
|
||
| `GET /ui` | Network access control | Basic HTML administration page. |
|
||
| `GET /api/admin/state` | `x-admin-secret` | Lists clients and keys with only a short key preview. |
|
||
| `GET /api/admin/generations?limit=100` | `x-admin-secret` | Returns aggregate totals, per-client totals, and recent generation timing/cost records (limit 1–200). |
|
||
| `POST /api/admin/client` | `x-admin-secret` | Form fields: `client_id`, `name`, and required `client_secret` (16–256 characters); creates or updates a client. |
|
||
| `POST /api/keys` | `auth-secret` | Form fields: `client_id`, `provider`, `api_key`, optional `priority`. Admin or owning client may add keys. |
|
||
| `DELETE /api/keys/{key_id}` | `auth-secret` | Deletes a key owned by the authenticated admin/client. |
|
||
|
||
## HTTP errors
|
||
|
||
Validation and authorization failures use FastAPI's `{"detail":"..."}` response shape. Common statuses:
|
||
|
||
| Status | Meaning |
|
||
| --- | --- |
|
||
| `400` | Invalid parameters, provider, or missing provider keys. |
|
||
| `401` | Missing or invalid admin/client secret. |
|
||
| `403` | Client is unknown, inactive, or has no secret configured. |
|
||
| `404` | Client/key/job was not found, or the job belongs to another client. |
|
||
| `413` | Request or image upload exceeds configured limits. |
|
||
| `415` | Unsupported, malformed, or MIME-mismatched image. |
|
||
| `502` | Upstream failure after applicable failover attempts. Response contains status metadata, not upstream response bodies. |
|
||
|
||
## Notes for test scripts
|
||
|
||
The admin generations endpoint shows total, completed, and failed runs; sum of known costs; per-client cost and timing; and recent per-generation request intake, upstream, and total seconds. Prompts, images, and full provider keys are not included in this history.
|
||
|
||
- Aggregate cost and average timing fields include completed generations only. Failed runs remain visible in counts and recent history, but do not affect those totals or averages.
|
||
- Use a dedicated test client and avoid real production provider keys.
|
||
- A request with no configured provider key is useful for checking async job behavior: it should be accepted, then finish as `failed` with `No active provider keys configured`.
|
||
- A real image generation test requires an active provider key and can incur provider charges.
|
||
- The service currently has one in-process worker. Polling jobs survive client disconnects and completed results survive process restarts, but an active job is marked failed if the proxy restarts.
|