12 KiB
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-secretto administrator endpoints, orauth-secretto provider-key endpoints. - Client secret: configured when creating a client. Send it as
x-client-secretwhen generating or polling. It can also be used asauth-secretfor 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:
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:
{"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.
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.
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):
{
"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.
Direct synchronous generation retains its JSON response with image bytes in b64_json. For async jobs, request include_result=false and use the binary download link described below. The legacy async include_result=true response remains supported and reconstructs Base64 from the stored image file.
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:
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):
{"job_id":"<uuid>","status":"queued","poll_url":"/v1/jobs/<uuid>"}
Poll every couple of seconds. To avoid transferring the generated image inside every completed status response, set include_result=false. Include both the client_id query parameter and x-client-secret header:
curl -sS "$BASE_URL/v1/jobs/<uuid>?client_id=booth-01&include_result=false" \
-H 'x-client-secret: replace-with-a-client-secret'
While running:
{"job_id":"<uuid>","status":"processing","created_at":1234567890.0,"updated_at":1234567891.0}
On success with include_result=false, status is completed and result contains generation metadata plus image descriptors. Each image descriptor includes a download_url for binary images, or the upstream url when the provider supplies a hosted URL. Download each binary image directly; do not Base64-decode it:
curl -sS "$BASE_URL/v1/jobs/<uuid>/images/0?token=<signed-token-from-download_url>" \
-o generated.webp
Async generated images are stored as WebP at quality 90 by default (OUTPUT_WEBP_QUALITY can be set from 1 to 100). The image endpoint and result descriptor use image/webp; save the downloaded bytes with a .webp extension. Convert to PNG or JPEG in the client only if a downstream tool requires that format.
GET /v1/jobs/{job_id}/images/{image_index}?token=... serves raw image bytes from the proxy's private output directory with the corresponding image MIME type and inline content disposition. Links in the completed-job response are signed, scoped to that job and image, and expire after one hour by default (IMAGE_LINK_TTL_SECONDS configures the lifetime). Open the full download_url directly in a browser; no client-secret header is needed. Treat the URL like a temporary password: anyone who has it can view that image until it expires. An authenticated client may also call the image route with client_id and x-client-secret instead of a token. Unknown and cross-client jobs return 404; an incomplete job returns 409; expired links or deleted images return 410.
Async WebP output files are stored under OUTPUT_DIR (default ./data/generated) with directory mode 0700 and file mode 0600. Image files are automatically deleted after 24 hours by default (OUTPUT_RETENTION_SECONDS), with a cleanup pass at startup and every five minutes. Job metadata, timing, and cost history remain in SQLite after image deletion; a deleted image endpoint returns 410. Completed and failed jobs clear their original prompt and uploaded image payload from SQLite. The database retains only compact result metadata and execution history; startup migrates older completed outputs to WebP and vacuums SQLite to reclaim freed pages.
For compatibility, include_result defaults to true; that legacy mode includes Base64 image data in the status response. New clients should always request include_result=false and download images from their download_url. On failure, status is failed and error contains a sanitized message.
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
failedwithNo 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.