Files
ph_api/docs/API.md

12 KiB
Raw Permalink Blame History

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:

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 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.