# gitpix API > AI image generation and editing via Google Gemini and OpenAI, with upscaling via Replicate. See /api/models for enabled models, /llms.txt for prose documentation, and /api/changelog for API version history. Project version: 3.1.4 (VERSION). API version: 2.3.0 (API_VERSION). Project releases and API contracts are versioned independently. Canonical Project website: https://toolbox.md/gitpix - https://gitpix.toolbox.md: Production: a Cloudflare Access authenticated session is required. - http://localhost:3000: Local development. ## Quickstart 1. `GET /api/models` — discover available models 2. `POST /api/conversations` — create a new chat 3. `PATCH /api/conversations/{id}` — set model, style, aspect ratio, image size, quality (optional; `quality` is OpenAI-only) 4. `POST /api/chat` — send a message (prompt + optional images) 5. `GET /api/conversations/{id}` — read messages, branches, and debug logs 6. `POST /api/messages/{id}/edit` — create a variant (branch from any user message) 7. `POST /api/messages/{id}/regenerate` — get a new AI response for the same prompt 8. `POST /api/conversations/{id}/switch-branch` — navigate between variants 9. `POST /api/messages/{id}/upscale` — upscale an AI-generated image (2x or 4x via Real-ESRGAN) 10. `GET /api/upscales/{id}` — fetch a previously upscaled image ## Versioning model — think git Conversations are trees, not linear lists. Understanding this is critical. - **Conversation** = repository - **Messages** = commits (each has a `parentMessageId`, forming a tree) - **Edit** = branch (`git checkout -b` from a specific commit — original stays, new variant created) - **Regenerate** = another branch from the same commit (same prompt, different AI output) - **`activeLeafId`** = HEAD (points to the current branch tip) - **Switch branch** = `git checkout` (move HEAD to a different branch tip) ### Two ways to generate **Multi-turn (`POST /api/chat`)** — like `git commit`. Adds to the current branch. AI sees all previous messages and images. Use for **iterative refinement**: "make the sky bluer", "now add a person". **Edit (`POST /api/messages/{id}/edit`)** — like `git checkout -b`. Creates an independent branch from that point. AI gets a fresh start with no knowledge of other branches. Use for **variations**: different styles, compositions, or interpretations of the same prompt. **Rule of thumb:** Want the AI to build on the previous result? Use chat. Want a fresh take? Use edit. ### Chaining images across conversations: use `{ref}` Once you've generated an image, its R2 URL is in the response (`response.images[0].url`). To re-use it as input to another conversation without re-encoding base64: ```bash curl -X POST http://localhost:3000/api/chat \ -H "Content-Type: application/json" \ -d '{ "conversationId": "...", "prompt": "make this brighter", "images": [ { "ref": "https://img.toolbox.md/gitpix/.webp", "label": "v7 body" } ] }' ``` `data` and `ref` are XOR per image — exactly one must be set. The server reuses the source R2 key in `messages.image_keys`, so deleting the user message will only remove the R2 object if no other message or upscale still references it (`media-service.ts` refcounts on delete). ## Changelog See also: machine-readable at [/api/changelog](./api/changelog). ### Project v3.1.4 - 2026-09-07 (API remains v2.3.0) - Restored Google model routing after upstream preview IDs were retired. Keep using `gemini-3-pro-image-preview` and `gemini-3.1-flash-image-preview` in API requests. Existing conversations, defaults and retry history remain valid. ### v2.3.0 — 2026-04-26 - Added `{ref}` form on image inputs: same-origin R2 URLs as alternative to base64 `{data}`. - Documentation correction (Project v3.1.3): the earlier claim of `/conversation/{id}` URL routing was incorrect. Open `/` and select a conversation from the sidebar; the API resource remains `/api/conversations/{id}`. - Fixed edit-message UI (broken since R2 migration); fixed sidebar hydration; fixed R2 cascade-delete leak. - Fixed `api-version.ts` constant drift (was returning "2.0.0" since 2026-04-25). ### 2026-04-25 — v2.2.0 - **Added:** `inflightSince` on every `ConversationSummary` row — epoch milliseconds when an in-flight generation request started, or `null` when idle. Agents polling the conversation list can use this to detect "this conversation is currently busy" without inspecting individual messages. - **Added:** `GET /api/projects` response shape is now `{ projects, recent }` — `recent` is up to 5 projects ranked by descendant chat activity, each carrying `id`, `name`, `parentId`, `fullPath`, `lastActivityAt`, `lastActiveConversationId`, and `lastImage` (256x256 WebP thumbnail or `null`). - **Added:** `ProjectRecentEntry` schema. - No breaking changes. ### 2026-04-25 — v2.1.0 (BREAKING) - **Removed**: `SendChatResponse.modelSwitched` field. OpenAI models now persist natively across multi-turn; there is no silent fallback to Gemini. - **OpenAI multi-turn**: `POST /api/chat`, `POST /api/messages/{id}/edit`, and `POST /api/messages/{id}/regenerate` now pass `[previousAiImage, ...newUserImages]` to `images.edit`, enabling true multi-turn editing on gpt-image-1.5. - **Model-aware sizes and qualities**: `GET /api/models` response includes `capabilities.sizes` and `capabilities.qualities` per model. Conversations gain an optional `quality` setting (OpenAI-only, default `"auto"`, valid: `"low"`, `"medium"`, `"high"`, `"auto"`). Set via `PATCH /api/conversations/{id}`. - **Fixed**: Banana Pro (`gemini-3-pro-image-preview`) was broken by an invalid `thinkingConfig` block — Vertex AI rejects external thinking config on that model. Fixed by guarding emission with `acceptsExternalThinkingConfig(modelId)`. - **Note**: gpt-image-2 is implemented but hidden (`enabled: false`) until the OpenAI account tier grants access. ### 2026-04-21 — v2.0.0 (BREAKING) - **Image response shape** moved to OpenAI-compatible object form: - `imageUrls: string[]` → `images: [{url, thumbUrl}]` (multi-image responses) - `imageUrl: string` → `image: {url, thumbUrl}` (singular upscale response) - Agent migration: `.imageUrls[i]` → `.images[i].url`; for thumbs, `.images[i].thumbUrl`. For singular upscale, `.imageUrl` → `.image.url`. - Every image URL now ships with a matching 256x256 WebP thumbnail at `.thumb.webp` (~30 KB). Use `thumbUrl` for sidebar/search/list views to avoid downloading full-size images during visual discovery. - `GET /api/conversations` items include `lastImage: {url, thumbUrl} | null` and `lastAssistantPreview: string | null`. - `GET /api/conversations/{id}` returns `lastImageMessageId` — the newest AI-message-with-image across all branches. - `GET /api/search` hits include `images: [{url, thumbUrl}]` — agents can scan visual results without fetching the full conversation. - `POST /api/conversations` body accepts `projectId` at creation time (previously only `PATCH`). ### 2026-04-21 — v1.2.0 - **Projects**: infinite-nesting folder tree. `GET/POST /api/projects`, `GET/PATCH/DELETE /api/projects/{id}` (cycle-protected, cascade delete). - **Search**: FTS5 full-text search. `GET /api/search?q=`. ``-wrapped snippets. Porter + unicode61 tokenizer (stemming + case/diacritic-insensitive; no typo tolerance). - **Settings**: global defaults row. `GET/PATCH /api/settings`. New conversations inherit. - **Star messages**: `messages.starred` flag. `PATCH /api/messages/{id}` body `{ starred: true | false }`. - **Delete messages**: `DELETE /api/messages/{id}` — cascades to descendants, R2 cleanup best-effort, `activeLeafId` falls back to parent if it pointed into the deleted subtree. - **Message detail**: `GET /api/messages/{id}` — includes `siblingInfo` + `availableUpscales`. - **Conversation list** filters: `?q`, `?projectId` (incl. `"none"`), `?starred` (`"true"` returns only conversations that contain at least one starred message), `?limit`, `?offset`. Response shape changed from `{conversations}` to `{conversations, total}`. ### 2026-04-20 — v1.1.0 (BREAKING) - Wire role: `ai` → `assistant` (DB unchanged; legacy `"ai"` still accepted on read). - Field rename: `modelName` → `model` in conversation settings. - Field: `imageSize` values now `"1024x1024" | "2048x2048" | "4096x4096"` (was `"1K"/"2K"/"4K"`). `"512"` dropped. - `POST /api/chat` body is now `application/json`; images inlined as `{ data: DataUrl, label?: string }` entries. - Error responses for schema validation use RFC 7807 `application/problem+json` with `code`, `retryable`, `retryAfter`, `retryScope`. - `POST /api/messages/{id}/edit`: body is now `{ content, images: [{data, label?}] }`. Images required, complete, no inheritance. - `PATCH /api/conversations/{id}` returns `{ conversation }` (full updated row) instead of `{ success: true }`. - New: `POST /api/images` (alias for chat), `Idempotency-Key` header (24h TTL), `GET /api/changelog`, `GET /openapi.json`. ### 2026-04-01 — v1.0.0 Initial public API. ## Endpoints (auto-generated — do not edit manually. Run `npm run build:llms`) ### POST `/api/admin/cleanup` **Purge imageless error-noise conversations** Deletes conversations matching a fixed 6-criterion filter (no AI images, no user-uploaded images, not in a project, no stars, no upscales, idle >24h). The filter is locked in service code — request parameters cannot widen it. HTTP body default is `{dryRun: true}` for preview safety; send `{dryRun: false}` to execute. A concurrent invocation returns 409. See docs/superpowers/specs/2026-04-22-admin-cleanup-tool-design.md for the full contract. **Request body (application/json):** ```json { "type": "object", "properties": { "dryRun": { "type": "boolean", "default": true } } } ``` **Response (200):** ```json { "type": "object", "properties": { "dryRun": { "type": "boolean", "description": "Echoes the effective dryRun value." }, "candidateCount": { "type": "integer", "minimum": 0, "description": "How many conversations matched the filter." }, "sampleTitles": { "type": "array", "items": { "type": "string" }, "maxItems": 5, "description": "Up to 5 sample conversation titles for operator context." }, "conversationsDeleted": { "type": "integer", "minimum": 0, "description": "Actual conversations removed. 0 when dryRun=true." }, "messagesDeleted": { "type": "integer", "minimum": 0, "description": "Messages cascade-deleted via FK. 0 when dryRun=true." }, "r2ObjectsDeleted": { "type": "integer", "minimum": 0, "description": "R2 objects successfully deleted. The current filter (criterion 2) excludes conversations with user-attachment images, so this is typically 0 today. Kept in the response shape for future operations." }, "r2ObjectsFailed": { "type": "integer", "minimum": 0, "description": "R2 delete calls that threw (best-effort; failures are logged but don't roll back the DB delete)." }, "durationMs": { "type": "integer", "minimum": 0, "description": "Wall-clock time for the entire operation." }, "lockAcquired": { "type": "boolean", "description": "True if the advisory lock was successfully acquired (or not needed — dry runs always report true). False cases return 409 before this response shape is used." } }, "required": [ "dryRun", "candidateCount", "sampleTitles", "conversationsDeleted", "messagesDeleted", "r2ObjectsDeleted", "r2ObjectsFailed", "durationMs", "lockAcquired" ] } ``` --- ### GET `/api/changelog` **Machine-readable changelog for API versions** **Response (200):** ```json { "type": "object", "properties": { "currentVersion": { "type": "string", "description": "The version currently running on this server." }, "entries": { "type": "array", "items": { "$ref": "#/components/schemas/ChangelogEntry" }, "description": "Most recent first." } }, "required": [ "currentVersion", "entries" ] } ``` --- ### POST `/api/chat` **Send a prompt and receive an AI-generated image** Primary image generation endpoint. Accepts optional reference images inlined as base64 data URLs. The response includes R2 URLs for generated images. **Request body (application/json):** ```json { "type": "object", "properties": { "conversationId": { "type": "string", "format": "uuid", "description": "Target conversation. Must exist.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "prompt": { "type": "string", "minLength": 1, "description": "Text input for image generation. This is the equivalent of OpenAI Images 'prompt' or Replicate 'input.prompt'." }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/ChatImageInput" }, "description": "Optional reference images. Each is uploaded to R2 and attached to the message." } }, "required": [ "conversationId", "prompt" ] } ``` **Response (200):** ```json { "type": "object", "properties": { "text": { "type": "string", "description": "AI text response (may be empty if image-only)." }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/R2Image" }, "description": "AI-generated images. Each entry is {url, thumbUrl} — full-size + 256x256 WebP thumbnail. As of v2.0.0 (replaces imageUrls: string[])." }, "userMessageId": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "aiMessageId": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "status": { "type": "string", "enum": [ "STOP", "MAX_TOKENS", "SAFETY", "RECITATION", "OTHER", "PROHIBITED_CONTENT", "SPII", "BLOCKLIST", "IMAGE_SAFETY", "MALFORMED_FUNCTION_CALL", "FINISH_REASON_UNSPECIFIED", "LANGUAGE", "BLOCKLIST_INPUT_IMAGE", "MIN_IMAGE_TOKENS_EXCEEDED", "MAX_IMAGE_TOKENS_EXCEEDED", "API_ERROR", "TIMEOUT" ], "description": "Gemini generation finish reason. STOP = success. All others are failures with statusMessage explaining." }, "statusMessage": { "type": [ "string", "null" ] } }, "required": [ "text", "images", "userMessageId", "aiMessageId", "status", "statusMessage" ] } ``` --- ### GET `/api/conversations` **List all conversations** **Response (200):** ```json { "type": "object", "properties": { "conversations": { "type": "array", "items": { "$ref": "#/components/schemas/ConversationSummary" } }, "total": { "type": "integer", "minimum": 0, "description": "Total matching conversations across all pages." } }, "required": [ "conversations", "total" ] } ``` --- ### POST `/api/conversations` **Create a new conversation** **Request body (application/json):** ```json { "type": "object", "properties": { "title": { "type": "string" }, "projectId": { "type": [ "string", "null" ], "format": "uuid", "description": "Assign the new conversation to a project. null or omitted = unassigned.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } } ``` **Response (200):** ```json { "type": "object", "properties": { "conversation": { "$ref": "#/components/schemas/ConversationSummary" } }, "required": [ "conversation" ] } ``` --- ### GET `/api/conversations/{id}` **Get conversation with messages, tree, retry status** **Response (200):** ```json { "type": "object", "properties": { "conversation": { "$ref": "#/components/schemas/ConversationSummary" }, "messages": { "type": "array", "items": { "$ref": "#/components/schemas/MessageSummary" } }, "messageTree": { "type": "array", "items": { "$ref": "#/components/schemas/MessageTreeNode" } }, "retryStatus": { "$ref": "#/components/schemas/RetryStatus" }, "debugLogs": { "type": "array", "items": { "type": "object", "additionalProperties": {} }, "description": "Raw debug_logs rows for this conversation (shape varies)." }, "lastImageMessageId": { "type": [ "string", "null" ], "format": "uuid", "description": "ID of the most recent AI message with an image, across all branches. Used by the UI to scroll+auto-switch-branch on load so the user sees their latest visual result. Null if the conversation has no AI images yet.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "required": [ "conversation", "messages", "messageTree", "retryStatus", "debugLogs", "lastImageMessageId" ] } ``` --- ### PATCH `/api/conversations/{id}` **Update conversation settings (title, model, aspectRatio, etc.)** **Request body (application/json):** ```json { "type": "object", "properties": { "styleDirective": { "type": [ "string", "null" ] }, "model": { "type": [ "string", "null" ], "description": "Default model for new messages in this conversation. If null, DEFAULT_MODEL is used." }, "aspectRatio": { "type": [ "string", "null" ], "enum": [ "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1", null ], "description": "Image aspect ratio. Banana Pro (gemini-3-pro-image-preview) supports only 1:1..21:9 — extreme ratios (1:4, 4:1, 1:8, 8:1) are Banana 2 only." }, "imageSize": { "type": [ "string", "null" ], "enum": [ "1024x1024", "2048x2048", "4096x4096", null ], "description": "Output image dimensions. Format is 'WIDTHxHEIGHT' in pixels. Height and width are always equal (1:1); aspect ratio is controlled by `aspectRatio` separately.", "example": "2048x2048" }, "searchGrounding": { "type": [ "boolean", "null" ] }, "thinkingLevel": { "type": [ "string", "null" ], "enum": [ "Minimal", "High", null ], "description": "Gemini thinking depth. Minimal = fast, few reasoning steps. High = slower, more deliberate. Input is case-insensitive; output is always TitleCase." }, "quality": { "type": [ "string", "null" ] }, "title": { "type": "string" }, "projectId": { "type": [ "string", "null" ], "format": "uuid", "description": "Assign to project, or null to clear.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } } ``` **Response (200):** ```json { "type": "object", "properties": { "conversation": { "$ref": "#/components/schemas/ConversationSummary" } }, "required": [ "conversation" ] } ``` --- ### DELETE `/api/conversations/{id}` **Delete a conversation and all its messages** **Response (200):** ```json { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] } ``` --- ### POST `/api/conversations/{id}/switch-branch` **Switch to a different branch in the conversation tree** **Request body (application/json):** ```json { "type": "object", "properties": { "messageId": { "type": "string", "format": "uuid", "description": "Target leaf message. Must belong to the conversation.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "required": [ "messageId" ] } ``` **Response (200):** ```json { "type": "object", "properties": { "activeLeafId": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "required": [ "activeLeafId" ] } ``` --- ### POST `/api/images` **Generate an image (alias for POST /api/chat)** Identical to POST /api/chat — intuitive URL for image generation workflows. **Request body (application/json):** ```json { "type": "object", "properties": { "conversationId": { "type": "string", "format": "uuid", "description": "Target conversation. Must exist.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "prompt": { "type": "string", "minLength": 1, "description": "Text input for image generation. This is the equivalent of OpenAI Images 'prompt' or Replicate 'input.prompt'." }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/ChatImageInput" }, "description": "Optional reference images. Each is uploaded to R2 and attached to the message." } }, "required": [ "conversationId", "prompt" ] } ``` **Response (200):** ```json { "type": "object", "properties": { "text": { "type": "string", "description": "AI text response (may be empty if image-only)." }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/R2Image" }, "description": "AI-generated images. Each entry is {url, thumbUrl} — full-size + 256x256 WebP thumbnail. As of v2.0.0 (replaces imageUrls: string[])." }, "userMessageId": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "aiMessageId": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "status": { "type": "string", "enum": [ "STOP", "MAX_TOKENS", "SAFETY", "RECITATION", "OTHER", "PROHIBITED_CONTENT", "SPII", "BLOCKLIST", "IMAGE_SAFETY", "MALFORMED_FUNCTION_CALL", "FINISH_REASON_UNSPECIFIED", "LANGUAGE", "BLOCKLIST_INPUT_IMAGE", "MIN_IMAGE_TOKENS_EXCEEDED", "MAX_IMAGE_TOKENS_EXCEEDED", "API_ERROR", "TIMEOUT" ], "description": "Gemini generation finish reason. STOP = success. All others are failures with statusMessage explaining." }, "statusMessage": { "type": [ "string", "null" ] } }, "required": [ "text", "images", "userMessageId", "aiMessageId", "status", "statusMessage" ] } ``` --- ### GET `/api/messages/{id}` **Get a single message with sibling info and upscales** **Response (200):** ```json { "allOf": [ { "$ref": "#/components/schemas/MessageSummary" }, { "type": "object", "properties": { "siblingInfo": { "$ref": "#/components/schemas/SiblingInfo" }, "availableUpscales": { "type": "array", "items": { "$ref": "#/components/schemas/UpscaleInfo" } } }, "required": [ "siblingInfo", "availableUpscales" ] } ] } ``` --- ### PATCH `/api/messages/{id}` **Toggle the starred flag on a message** Simple body {starred: boolean}. For renaming/branching see /edit. **Request body (application/json):** ```json { "type": "object", "properties": { "starred": { "type": "boolean" } }, "required": [ "starred" ] } ``` **Response (200):** ```json { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] } }, "required": [ "success" ] } ``` --- ### DELETE `/api/messages/{id}` **Delete a message and all its descendants (cascade + R2 cleanup)** Recursively deletes the message subtree, removes R2 objects, and moves activeLeafId to the parent if it was inside the deleted subtree. **Response (200):** ```json { "type": "object", "properties": { "deleted": { "type": "integer", "minimum": 1, "description": "Total messages deleted (message + descendants)." }, "activeLeafId": { "type": [ "string", "null" ], "format": "uuid", "description": "New activeLeafId if it was in the deleted subtree, else unchanged.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "required": [ "deleted", "activeLeafId" ] } ``` --- ### POST `/api/messages/{id}/edit` **Edit a user message (creates a branch)** **Request body (application/json):** ```json { "type": "object", "properties": { "content": { "type": "string", "minLength": 1, "description": "New prompt text." }, "images": { "type": "array", "items": { "$ref": "#/components/schemas/ChatImageInput" }, "description": "Complete final image array. NOT inherited from original message — sender must resend all images they want to keep." } }, "required": [ "content", "images" ] } ``` **Response (200):** ```json { "type": "object", "properties": { "userMessage": { "$ref": "#/components/schemas/MessageSummary" }, "aiMessage": { "$ref": "#/components/schemas/MessageSummary" }, "activeLeafId": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "status": { "type": "string" }, "statusMessage": { "type": [ "string", "null" ] } }, "required": [ "userMessage", "aiMessage", "activeLeafId", "status", "statusMessage" ] } ``` --- ### POST `/api/messages/{id}/regenerate` **Regenerate an AI response (creates a sibling)** **Request body (application/json):** ```json { "type": "object", "properties": { "model": { "type": "string", "description": "Override model for this regenerate. If omitted, uses the conversation's current model." } } } ``` **Response (200):** ```json { "type": "object", "properties": { "userMessage": { "$ref": "#/components/schemas/MessageSummary" }, "aiMessage": { "$ref": "#/components/schemas/MessageSummary" }, "activeLeafId": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "status": { "type": "string" }, "statusMessage": { "type": [ "string", "null" ] } }, "required": [ "userMessage", "aiMessage", "activeLeafId", "status", "statusMessage" ] } ``` --- ### POST `/api/messages/{id}/upscale` **Upscale an AI-generated image (2x or 4x)** **Request body (application/json):** ```json { "type": "object", "properties": { "scale": { "anyOf": [ { "type": "number", "enum": [ 2 ] }, { "type": "number", "enum": [ 4 ] } ], "description": "2 = 2x upscale, 4 = 4x upscale. Upscaled via Replicate Real-ESRGAN." } }, "required": [ "scale" ] } ``` **Response (200):** ```json { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "messageId": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "scale": { "anyOf": [ { "type": "number", "enum": [ 2 ] }, { "type": "number", "enum": [ 4 ] } ] }, "image": { "allOf": [ { "$ref": "#/components/schemas/R2Image" }, { "description": "The upscaled image (full-size + thumbnail). As of v2.0.0 (replaces imageUrl: string)." } ] }, "provider": { "type": "string" }, "model": { "type": "string" }, "durationMs": { "type": "integer", "minimum": 0 }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp.", "example": "2026-04-20T14:23:07.000Z" } }, "required": [ "id", "messageId", "scale", "image", "provider", "model", "durationMs", "createdAt" ] } ``` --- ### GET `/api/models` **List available models with health + retry status** **Response (200):** ```json { "type": "object", "properties": { "models": { "type": "array", "items": { "$ref": "#/components/schemas/ModelInfo" } }, "default": { "type": "string", "description": "Default model ID." }, "retryStatus": { "$ref": "#/components/schemas/RetryStatus" } }, "required": [ "models", "default", "retryStatus" ] } ``` --- ### GET `/api/projects` **List all projects as a nested tree** Returns the full project tree with computed paths and per-node conversation counts. Infinite nesting supported. **Response (200):** ```json { "type": "object", "properties": { "projects": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectNode" }, "description": "Full project tree, roots sorted by name." }, "recent": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectRecentEntry" }, "description": "Up to 5 projects ranked by most recent descendant chat activity. Each carries leaf name, full path, and a thumbnail." } }, "required": [ "projects", "recent" ] } ``` --- ### POST `/api/projects` **Create a project (root or child)** Create a new project. Omit parentId (or pass null) for a root project. **Request body (application/json):** ```json { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 100 }, "parentId": { "type": [ "string", "null" ], "format": "uuid", "description": "Parent project. Omit or null for root.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "required": [ "name" ] } ``` **Response (200):** ```json { "type": "object", "properties": { "project": { "$ref": "#/components/schemas/ProjectNode" } }, "required": [ "project" ] } ``` --- ### GET `/api/projects/{id}` **Get a single project with its subtree** **Response (200):** ```json { "type": "object", "properties": { "project": { "$ref": "#/components/schemas/ProjectNode" } }, "required": [ "project" ] } ``` --- ### PATCH `/api/projects/{id}` **Rename or move a project** Cycle-protected: refuses to move a project under its own descendant. Duplicate sibling names are rejected with 409. **Request body (application/json):** ```json { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 100 }, "parentId": { "type": [ "string", "null" ], "format": "uuid", "description": "New parent. null = move to root. Must not be a descendant.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } } ``` **Response (200):** ```json { "type": "object", "properties": { "project": { "$ref": "#/components/schemas/ProjectNode" } }, "required": [ "project" ] } ``` --- ### DELETE `/api/projects/{id}` **Delete a project (cascades to descendants)** Deletes the project and all descendants. Conversations assigned to any deleted project get project_id = NULL (they survive). **Response (200):** ```json { "type": "object", "properties": { "deletedProjectIds": { "type": "array", "items": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "description": "IDs of all deleted projects (cascade)." }, "unassignedConversations": { "type": "integer", "minimum": 0, "description": "Number of conversations whose project_id was cleared." } }, "required": [ "deletedProjectIds", "unassignedConversations" ] } ``` --- ### GET `/api/search` **Full-text search across messages and conversation titles** Uses the messages_fts FTS5 virtual table for message hits and a LIKE scan for title hits. Snippets include tags. Results deduped by conversation. **Response (200):** ```json { "type": "object", "properties": { "query": { "type": "string" }, "total": { "type": "integer", "minimum": 0 }, "hits": { "type": "array", "items": { "$ref": "#/components/schemas/SearchHit" } } }, "required": [ "query", "total", "hits" ] } ``` --- ### GET `/api/settings` **Get global default settings** Returns the single global settings row. New conversations inherit these values. Per-user settings arrive with auth. **Response (200):** ```json { "type": "object", "properties": { "model": { "type": [ "string", "null" ] }, "aspectRatio": { "type": [ "string", "null" ], "enum": [ "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1", null ], "description": "Image aspect ratio. Banana Pro (gemini-3-pro-image-preview) supports only 1:1..21:9 — extreme ratios (1:4, 4:1, 1:8, 8:1) are Banana 2 only." }, "imageSize": { "type": [ "string", "null" ], "enum": [ "1024x1024", "2048x2048", "4096x4096", null ], "description": "Output image dimensions. Format is 'WIDTHxHEIGHT' in pixels. Height and width are always equal (1:1); aspect ratio is controlled by `aspectRatio` separately.", "example": "2048x2048" }, "styleDirective": { "type": [ "string", "null" ] }, "thinkingLevel": { "type": [ "string", "null" ], "enum": [ "Minimal", "High", null ], "description": "Gemini thinking depth. Minimal = fast, few reasoning steps. High = slower, more deliberate. Input is case-insensitive; output is always TitleCase." }, "quality": { "type": [ "string", "null" ] }, "searchGrounding": { "type": [ "boolean", "null" ] }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp.", "example": "2026-04-20T14:23:07.000Z" } }, "required": [ "model", "aspectRatio", "imageSize", "styleDirective", "thinkingLevel", "quality", "searchGrounding", "updatedAt" ] } ``` --- ### PATCH `/api/settings` **Update global default settings** Update any subset of fields. Only provided fields are changed. Pass null to clear a field. **Request body (application/json):** ```json { "type": "object", "properties": { "model": { "type": [ "string", "null" ] }, "aspectRatio": { "type": [ "string", "null" ], "enum": [ "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1", null ], "description": "Image aspect ratio. Banana Pro (gemini-3-pro-image-preview) supports only 1:1..21:9 — extreme ratios (1:4, 4:1, 1:8, 8:1) are Banana 2 only." }, "imageSize": { "type": [ "string", "null" ], "enum": [ "1024x1024", "2048x2048", "4096x4096", null ], "description": "Output image dimensions. Format is 'WIDTHxHEIGHT' in pixels. Height and width are always equal (1:1); aspect ratio is controlled by `aspectRatio` separately.", "example": "2048x2048" }, "styleDirective": { "type": [ "string", "null" ] }, "thinkingLevel": { "type": [ "string", "null" ], "enum": [ "Minimal", "High", null ], "description": "Gemini thinking depth. Minimal = fast, few reasoning steps. High = slower, more deliberate. Input is case-insensitive; output is always TitleCase." }, "quality": { "type": [ "string", "null" ] }, "searchGrounding": { "type": [ "boolean", "null" ] } } } ``` **Response (200):** ```json { "type": "object", "properties": { "model": { "type": [ "string", "null" ] }, "aspectRatio": { "type": [ "string", "null" ], "enum": [ "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "1:4", "4:1", "1:8", "8:1", null ], "description": "Image aspect ratio. Banana Pro (gemini-3-pro-image-preview) supports only 1:1..21:9 — extreme ratios (1:4, 4:1, 1:8, 8:1) are Banana 2 only." }, "imageSize": { "type": [ "string", "null" ], "enum": [ "1024x1024", "2048x2048", "4096x4096", null ], "description": "Output image dimensions. Format is 'WIDTHxHEIGHT' in pixels. Height and width are always equal (1:1); aspect ratio is controlled by `aspectRatio` separately.", "example": "2048x2048" }, "styleDirective": { "type": [ "string", "null" ] }, "thinkingLevel": { "type": [ "string", "null" ], "enum": [ "Minimal", "High", null ], "description": "Gemini thinking depth. Minimal = fast, few reasoning steps. High = slower, more deliberate. Input is case-insensitive; output is always TitleCase." }, "quality": { "type": [ "string", "null" ] }, "searchGrounding": { "type": [ "boolean", "null" ] }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp.", "example": "2026-04-20T14:23:07.000Z" } }, "required": [ "model", "aspectRatio", "imageSize", "styleDirective", "thinkingLevel", "quality", "searchGrounding", "updatedAt" ] } ``` --- ### GET `/api/upscales/{id}` **Get an upscaled image record** **Response (200):** ```json { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "messageId": { "type": "string", "format": "uuid", "description": "UUIDv4 identifier.", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "scale": { "anyOf": [ { "type": "number", "enum": [ 2 ] }, { "type": "number", "enum": [ 4 ] } ] }, "image": { "allOf": [ { "$ref": "#/components/schemas/R2Image" }, { "description": "The upscaled image (full-size + thumbnail). As of v2.0.0 (replaces imageUrl: string)." } ] }, "provider": { "type": "string" }, "model": { "type": "string" }, "durationMs": { "type": "integer", "minimum": 0 }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp.", "example": "2026-04-20T14:23:07.000Z" } }, "required": [ "id", "messageId", "scale", "image", "provider", "model", "durationMs", "createdAt" ] } ``` --- ## Retry & Error Handling ### Error Response Fields All error responses from generation endpoints (`POST /api/chat`, `POST /api/messages/{id}/edit`, `POST /api/messages/{id}/regenerate`) now include: - `retryable` (boolean) — whether retrying is likely to succeed - `retryAfter` (number | null) — suggested seconds to wait before retrying, based on recent failure patterns. null for request-scoped errors. - `retryScope` ("model" | "project" | "request") — blast radius of the error - `model` (string) — the model ID that failed (e.g. "gemini-3.1-flash-image-preview") ### Error Types Table | status | retryable | retryScope | retryAfter | meaning | |--------|-----------|------------|------------|---------| | OVERLOADED | true | model | from backoff ladder | Gemini servers under heavy load | | RATE_LIMIT | true | project | from backoff ladder | Project rate limit exceeded | | API_ERROR | true | request | null | Server error — try again or simplify | | TIMEOUT | true | model | null | Request timed out | | INVALID_REQUEST | false | request | null | Bad request — fix input | | AUTH_ERROR | false | request | null | Auth failed | | NOT_FOUND | false | request | null | Model not found | ### Checking Retry State - `GET /api/conversations/{id}` includes `retryStatus` with `models: Record` showing per-model `recentFailures` and `retryAfter` - `GET /api/models` includes `health` ("healthy" / "degraded" / "down") and `retryAfter` per model - Each model has independent health and failure tracking. When one model is down, others may still work. ### Recommended Agent Workflow 1. Check `GET /api/models` — if a model's health is "down", wait `retryAfter` seconds before sending 2. Send request — if error with `retryable: true`, wait `retryAfter` seconds and retry 3. To check current state: `GET /api/conversations/{id}` includes `retryStatus` with per-model tracking ### Notes - `retryAfter` is a suggestion based on recent failure patterns, not a hard block - `retryScope: "model"` means failures are tracked per model (one model being overloaded doesn't block other models) - Rapid retries during outages waste quota — respect `retryAfter` - `retryAfter` is computed from the count of recent errors (last 30 minutes) for the failing model - Backoff ladder (based on failure count in last 30 min): 1 → 30s, 2 → 60s, 3 → 180s, 4 → 300s, 5 → 600s, 6+ → 1800s - Failures decay naturally as they age out of the 30-minute window (NOT reset on success) - Request-scoped errors (500, timeouts, safety blocks) return `retryAfter: null` **Note on 429 errors:** - Monthly spending cap exhaustion returns `retryable: false` (permanent until billing is updated) - Daily RPD quota exhaustion returns `retryAfter` parsed from Gemini's "Please retry in Xh Ym Zs" message - Short-window rate limits return `retryable: true, retryAfter: 60, retryScope: "project"` ## Concepts ### Upscaling AI-generated images can be upscaled to higher resolutions using Real-ESRGAN (via Replicate API). Upscales are stored separately from messages — they are post-processing artifacts, not new messages in the conversation tree. Each AI message can have up to two upscales (2x and 4x). The conversation endpoint (`GET /api/conversations/{id}`) includes `availableUpscales` per message (array of `{id, scale}`) so you know which scales exist without loading the image data. Strategy: generate at 1K (fast, cheap) then upscale to 2K/4K for better results than native high-res generation. ### Style directives Set `styleDirective` on a conversation (e.g. "watercolor painting", "Minecraft style"). It is automatically prepended to every prompt sent to Gemini. Persists until changed or cleared. ### Model selection Set `model` on a conversation via `PATCH /api/conversations/{id}`. Available models from `GET /api/models`. Defaults to Banana 2 (`gemini-3.1-flash-image-preview`). Currently enabled models: - **Banana 2** (`gemini-3.1-flash-image-preview`) — default. Supports thinking, search grounding, all aspect ratios/sizes. - **Banana Pro** (`gemini-3-pro-image-preview`) — higher quality, reasoning always-on internally. Aspect ratios 1:1 through 21:9 only (no extreme ratios). - **GPT Image 1.5** (`gpt-image-1.5`) — OpenAI model. Supports multi-turn editing, quality setting. No thinking/search grounding. ### Aspect ratio and image size Set `aspectRatio` (e.g. `"16:9"`) and `imageSize` (`"1024x1024"`, `"2048x2048"`, or `"4096x4096"`) on a conversation. Pixel strings are the v1.1+ canonical values — the legacy `"1K"/"2K"/"4K"` aliases are still accepted on input for back-compat but are never returned. ### Search grounding Set `searchGrounding: true` for Google Search during generation. Improves accuracy for real-world subjects. Adds latency. Off by default. ### Thinking level Set `thinkingLevel` to "Minimal" (default, faster) or "High" (better quality, slower). Only effective on Banana 2 (`gemini-3.1-flash-image-preview`); Banana Pro reasons internally and Vertex AI rejects any external `thinkingConfig` block on that model. ### Quality (OpenAI models only) Set `quality` on a conversation via `PATCH /api/conversations/{id}`. Valid values: `"low"`, `"medium"`, `"high"`, `"auto"` (default). Only applied when using an OpenAI model (gpt-image-1.5). Ignored for Gemini models. Mirrors `thinkingLevel` but for OpenAI image quality tuning: `"low"` is fastest and cheapest, `"high"` is sharpest, `"auto"` lets OpenAI decide. ### In-flight generation tracking Every conversation row returned by `GET /api/conversations` includes `inflightSince` — epoch milliseconds when a generation request started, or `null` when idle. Agents that poll the conversation list can use this to detect "this conversation is currently busy with chat/edit/regenerate" without inspecting individual messages. The value comes from a server-side `generations` table that is pruned on every list fetch via a 5-minute abandonment window. Idempotent retries (`Idempotency-Key` header) do not affect the table — the cached response is returned before any in-flight row is created. ### Images Send images via `POST /api/chat` (or the `/api/images` alias) using `application/json`. Images are base64 data URLs inlined in the body as `images: [{data, label?}]`. The server uploads each to Cloudflare R2 and stores only the object key (`gitpix/.webp`) in SQLite. API responses return `imageUrls` — an array of public URLs at `https://img.toolbox.md/gitpix/.webp` — which you can pass directly to `` or `fetch()`. The underlying storage is a platform-shared bucket (`toolboxmd-images-*`) with a product-scoped prefix (`gitpix/...`) so multiple toolbox.md apps can coexist. **Legacy rows**: messages older than the R2 migration (2026-04-20) used `imageData` — a JSON array of base64 data URLs — and may still present as such on a small set of unmigrated rows. Prefer `imageUrls` when present; fall back to `imageData` otherwise. For upscales, prefer `imageUrl` (singular) over `imageData`. ### Image labels Use `imageLabels` to name each attached image (e.g. `["Product photo", "Background"]`). Labels tell the AI what each image contains so you can reference them by name in your prompt. Without labels, images are named "Image 1", "Image 2" generically. Good labels significantly improve multi-image results. **Conditional wrapping:** If the request contains 0 or 1 image (counting any previous AI image being carried forward), the server sends your prompt to Gemini as-is — no `[Context: ...]` block, no per-image `[Label]` markers. This matches Google AI Studio behavior for single-image generation. When 2 or more images are present, the server prepends a `[Context: ...]` block listing the labels so the model can disambiguate (e.g. "use 'Product' on 'Background'") and inserts a `[Label]` text marker before each image. The `POST /api/messages/{id}/regenerate` endpoint honors the original message's labels by reading them server-side from the parent user message. The `POST /api/messages/{id}/edit` endpoint requires the caller to send the complete final `images` and `imageLabels` arrays — it does not infer from the original message. The frontend handles this transparently in the edit-in-input UI. ### Multi-turn image editing When a text-only prompt is sent and there is a previous AI-generated image in the conversation, that image is automatically included as context. This enables iterative editing: "make it more red", "add a hat", etc. ### Debug logs Every AI response generates a debug log (enhanced prompt, timing, model, image counts, generation settings). Returned with the conversation via `GET /api/conversations/{id}`. ### Idempotency Send `Idempotency-Key: ` on `POST /api/chat`, `POST /api/images`, `POST /api/messages/{id}/edit`, or `POST /api/messages/{id}/regenerate`. If you repeat a request with the same key within 24h, you get the cached response back with `Idempotent-Replay: true`. Use a UUID (or any unique string) per logical request — not per retry attempt. This prevents duplicate image generation (and charges) when an agent retries after a timeout. ### Role on the wire Messages use `role: "user" | "assistant"` in the API boundary (OpenAI / Anthropic / Cohere convention). The underlying DB still stores `"ai"` for legacy reasons, but you should always read and write `"assistant"` over HTTP. ### Error shape (RFC 7807) Schema validation errors and not-found errors return `application/problem+json`: ```json { "type": "about:blank", "title": "Invalid request", "status": 400, "detail": "prompt: Prompt cannot be empty", "code": "SCHEMA_VALIDATION", "field": "prompt", "hint": "See /openapi.json for the schema.", "docs": "https://gitpix.toolbox.md/llms.txt", "retryable": false, "retryAfter": null, "retryScope": "request" } ``` `code` values: `SCHEMA_VALIDATION`, `NOT_FOUND`, `INVALID_FIELD`, `RATE_LIMITED`, `UPSTREAM_FAILURE`, `INTERNAL`, `CONTENT_BLOCKED`, `IDEMPOTENCY_CONFLICT`. Upstream Gemini/OpenAI errors (the generation itself failed) are still persisted as AI messages in DB so they appear in conversation history — see "Retry & Error Handling" above. ### Generation status codes Every generation response includes `status` and `statusMessage`. Check `status === "STOP"` for success. - `STOP` — success. `statusMessage` is `null`. - `SAFETY` / `IMAGE_SAFETY` — blocked by safety filters. Rephrase. - `RECITATION` / `IMAGE_RECITATION` — blocked, potential copyright. - `OTHER` / `IMAGE_OTHER` — blocked, often copyrighted characters. Describe differently. - `PROHIBITED_CONTENT` / `IMAGE_PROHIBITED_CONTENT` — prohibited content. - `MAX_TOKENS` — response cut short. - `BLOCKLIST` — prompt matched a blocked term. - `SPII` — sensitive personal information detected. - `BLOCKED_SAFETY`, `BLOCKED_OTHER` — prompt blocked before generation. - `NO_CANDIDATES` — no response generated. ### Error responses API errors (429, 500, timeout) are saved as AI messages and include a `geminiError` field: ```json { "status": "RATE_LIMIT", "statusMessage": "Rate limited by Gemini API. Wait a moment and try again.", "geminiError": { "code": 429, "message": "Resource exhausted", "status": "RESOURCE_EXHAUSTED" } } ``` Error status values: `RATE_LIMIT`, `OVERLOADED`, `TIMEOUT`, `NOT_FOUND`, `INVALID_REQUEST`, `AUTH_ERROR`, `API_ERROR`. ### Idempotency-Key (current limitation) `POST /api/chat`, `/api/messages/{id}/edit`, `/api/messages/{id}/regenerate`, and `/api/images` accept an `Idempotency-Key` header for safe retries. **The cache is keyed on the header value alone — there is no body fingerprinting.** This means: - Reusing the same key with a CHANGED body returns the cached response from the first call. **Vary the key when you vary the body.** - Failed requests (4xx) are NOT cached — retries re-execute and may produce a different result. - Cache TTL is 24h. A future change may add SHA-256 body fingerprinting and 409 IDEMPOTENCY_CONFLICT on mismatch (RFC 8941 style). Until then, treat the key as a body-bound nonce on the client side. ### Conversation navigation Open `https://gitpix.toolbox.md/` and select a conversation from the sidebar. The selected conversation is held in UI state; `/conversation/{id}` is not an implemented frontend route, so direct conversation links and URL-based refresh or browser history are not supported. Agents can read a conversation through the separate API resource, `GET /api/conversations/{id}`. ## Prompt Writing Guide ### Core Principle Describe the scene as a narrative paragraph, not a keyword list. Bad: `cat, sunset, beach, golden hour, 85mm` Good: `A siamese cat sitting on a sandy beach at golden hour, the warm sunset light casting long shadows across the sand. Shot with an 85mm portrait lens with soft bokeh in the background.` ### Prompt Structure Template Include in order: **subject**, **composition/action**, **setting**, **lighting**, **style/medium**, **camera details**, **mood**, **format hint**. ``` A [style/medium] [shot type] of [subject], [action], set in [environment]. Illuminated by [lighting], creating a [mood] atmosphere. [Camera details]. [Format hint]. ``` ### Photorealistic Portraits ``` A photorealistic close-up portrait of an elderly Japanese ceramicist with deep, sun-etched wrinkles and a warm, knowing smile. He is carefully inspecting a freshly glazed tea bowl. The setting is his rustic, sun-drenched workshop. Soft, golden hour light streaming through a window. Captured with an 85mm portrait lens with soft bokeh. Vertical portrait orientation. ``` ### Stylized Illustrations ``` A kawaii-style sticker of a happy red panda wearing a tiny bamboo hat. It's munching on a green bamboo leaf. Bold, clean outlines, simple cel-shading, vibrant color palette. White background. ``` ### Text Rendering Gemini renders text accurately. Be explicit about content, font, and placement. ``` Create a modern, minimalist logo for a coffee shop called 'The Daily Grind'. Clean, bold, sans-serif font. A simple stylized coffee bean icon integrated with the text. Black and white. ``` ``` A photo of a glossy magazine cover, minimal blue cover with large bold words Nano Banana in serif font. A portrait of a person in a sleek dress, playfully holding the number 2. Issue number and 'Feb 2026' date in the corner with a barcode. On a shelf against an orange plastered wall. ``` ### Product Photography ``` A high-resolution studio-lit product photo of a minimalist ceramic coffee mug in matte black on polished concrete. Three-point softbox lighting, soft diffused highlights. Slightly elevated 45-degree angle. Sharp focus on rising steam. Square image. ``` ### Multi-turn: Character Consistency Establish a character in turn 1, then place them in new contexts: ``` Turn 1: Create a photorealistic image of a siamese cat with a green left eye and a blue right one Turn 2: Side view of that cat in a tropical forest, eating a banana under the stars Turn 3: Same cat, well dressed in a fancy restaurant eating a fancy banana ``` The model remembers appearance across turns without re-describing it. ### Multi-turn: Iterative Refinement ``` Turn 1: Edit this image to make it look like a cartoon Turn 2: But make it old-school line drawing style ``` Refinement phrases that work: "make the lighting warmer", "keep everything but change the expression", "what other colors would work?" ### Style Transfer ``` Transform this photograph of a city street at night into Van Gogh's 'Starry Night' style. Preserve the original composition but render with swirling impasto brushstrokes in deep blues and bright yellows. ``` ### Inpainting ``` Using this living room image, change only the blue sofa to a vintage brown leather chesterfield. Keep everything else unchanged. ``` ### Compositing Multiple Images ``` Take the dress from 'Product photo' and let the woman from 'Fashion model' wear it. Professional e-commerce full-body shot with matched lighting. [+ dress.png + model.png] ``` ### Search Grounding Enable `searchGrounding: true` for real-world subjects: ``` A detailed painting of a Timareta Thelxione butterfly resting on a flower ``` ### Camera Control - **Wide-angle** — expansive scene, slight edge distortion - **Macro** — extreme close-up, shallow depth of field - **Low-angle** — looking up, makes subject imposing - **85mm portrait** — flattering compression, soft background - **Dutch angle** — tilted frame, creates tension - **Golden hour** — warm, directional, long shadows - **Rembrandt lighting** — dramatic triangle of light on cheek ### Tips 1. **Be specific** — "ornate elven plate armor with silver leaf patterns" beats "fantasy armor" 2. **Narrative > keywords** — describe scenes as paragraphs 3. **Name the style** — "kawaii", "noir", "Pop Art", "Van Gogh" 4. **Include lighting** — "golden hour", "softbox", "harsh fluorescent" 5. **Specify camera** — "85mm", "wide-angle", "macro", "low-angle" 6. **State format** — "Square", "Vertical portrait", "16:9" 7. **Positive framing** — describe what you want, not what you don't 8. **Iterate** — start with a base, refine with short follow-ups 9. **Quote text** — put exact text to render in single quotes 10. **Use labels** — name your reference images for precise multi-image prompts