{
  "openapi": "3.1.0",
  "info": {
    "title": "gitpix",
    "version": "2.3.0",
    "x-project-version": "3.1.4",
    "x-project-website": "https://toolbox.md/gitpix",
    "description": "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.",
    "contact": {
      "name": "gitpix",
      "url": "https://gitpix.toolbox.md/"
    }
  },
  "servers": [
    {
      "url": "https://gitpix.toolbox.md",
      "description": "Production: a Cloudflare Access authenticated session is required"
    },
    {
      "url": "http://localhost:3000",
      "description": "Local development"
    }
  ],
  "tags": [
    {
      "name": "Generation",
      "description": "Prompt → AI image. The core endpoint."
    },
    {
      "name": "Messages",
      "description": "Per-message operations: edit, regenerate, upscale."
    },
    {
      "name": "Conversations",
      "description": "CRUD + tree navigation + branch switching."
    },
    {
      "name": "Models",
      "description": "Available AI models + health."
    },
    {
      "name": "Upscales",
      "description": "Post-generation upscaled image records."
    },
    {
      "name": "Projects",
      "description": "Infinitely-nested folders for organizing conversations."
    },
    {
      "name": "Settings",
      "description": "Global defaults applied to new conversations."
    },
    {
      "name": "Search",
      "description": "Full-text search across messages and titles."
    },
    {
      "name": "Meta",
      "description": "Versioning + changelog."
    },
    {
      "name": "Admin",
      "description": "Administrative operations (cleanup, maintenance)."
    }
  ],
  "components": {
    "schemas": {
      "ApiError": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "default": "about:blank",
            "description": "URI identifying the error type. 'about:blank' if not specified."
          },
          "title": {
            "type": "string",
            "description": "Short, human-readable summary."
          },
          "status": {
            "type": "integer",
            "minimum": 100,
            "maximum": 599,
            "description": "HTTP status code."
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation specific to this occurrence."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable error code (e.g. 'SCHEMA_VALIDATION', 'NOT_FOUND', 'RATE_LIMITED')."
          },
          "field": {
            "type": "string",
            "description": "If the error relates to a specific field, its name."
          },
          "hint": {
            "type": "string",
            "description": "Actionable suggestion for fixing the error."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Link to documentation for this error."
          },
          "retryable": {
            "type": "boolean",
            "description": "Whether the client should retry."
          },
          "retryAfter": {
            "type": [
              "number",
              "null"
            ],
            "description": "Seconds to wait before retry. Null if unknown or not retryable."
          },
          "retryScope": {
            "type": "string",
            "enum": [
              "model",
              "project",
              "request"
            ],
            "description": "Scope of the backoff: model = per-model, project = all requests, request = just this call."
          }
        },
        "required": [
          "title",
          "status",
          "code",
          "retryable",
          "retryAfter",
          "retryScope"
        ]
      },
      "SendChatResponse": {
        "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"
        ]
      },
      "R2Image": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public URL for an image stored on R2. Format: https://img.toolbox.md/gitpix/{uuid}.webp.",
            "example": "https://img.toolbox.md/gitpix/a1b2c3d4-e5f6-7890-abcd-ef1234567890.webp"
          },
          "thumbUrl": {
            "type": "string",
            "format": "uri",
            "description": "Public URL for a 256x256 WebP thumbnail at img.toolbox.md/gitpix/{uuid}.thumb.webp. Convention-derived from the primary URL (.webp -> .thumb.webp). Non-nullable as of v2.0.0.",
            "example": "https://img.toolbox.md/gitpix/a1b2c3d4-e5f6-7890-abcd-ef1234567890.thumb.webp"
          }
        },
        "required": [
          "url",
          "thumbUrl"
        ]
      },
      "SendChatBody": {
        "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"
        ]
      },
      "ChatImageInput": {
        "type": "object",
        "properties": {
          "data": {
            "type": "string",
            "pattern": "^data:[\\w.+-]+\\/[\\w.+-]+;base64,",
            "description": "Base64 data URL of the image. Server uploads to R2 and stores the key. XOR with ref — exactly one must be set.",
            "example": "data:image/webp;base64,UklGRh..."
          },
          "ref": {
            "type": "string",
            "pattern": "^https:\\/\\/img\\.toolbox\\.md\\/gitpix\\/[a-f0-9-]+\\.(webp|png|jpg|gif)$",
            "description": "Same-origin R2 image URL (https://img.toolbox.md/gitpix/<uuid>.<ext>). Server reuses the existing R2 object; no re-upload. XOR with data.",
            "example": "https://img.toolbox.md/gitpix/a1b2c3d4-e5f6-7890-abcd-ef1234567890.webp"
          },
          "label": {
            "type": "string",
            "description": "Optional label for this image, used in the prompt context wrapper (e.g. 'Reference photo'). Only applied when >= 2 images."
          }
        },
        "additionalProperties": false
      },
      "GetMessageResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/MessageSummary"
          },
          {
            "type": "object",
            "properties": {
              "siblingInfo": {
                "$ref": "#/components/schemas/SiblingInfo"
              },
              "availableUpscales": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/UpscaleInfo"
                }
              }
            },
            "required": [
              "siblingInfo",
              "availableUpscales"
            ]
          }
        ]
      },
      "SiblingInfo": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "minimum": 0
          },
          "count": {
            "type": "integer",
            "minimum": 1
          },
          "siblingIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "description": "UUIDv4 identifier.",
              "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
            }
          }
        },
        "required": [
          "index",
          "count",
          "siblingIds"
        ]
      },
      "UpscaleInfo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "scale": {
            "anyOf": [
              {
                "type": "number",
                "enum": [
                  2
                ]
              },
              {
                "type": "number",
                "enum": [
                  4
                ]
              }
            ]
          }
        },
        "required": [
          "id",
          "scale"
        ]
      },
      "MessageSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "role": {
            "type": "string",
            "enum": [
              "user",
              "assistant"
            ],
            "description": "Message role on the wire. Database stores 'ai' for historical reasons; API always emits and accepts 'assistant'. This matches OpenAI / Anthropic / Cohere conventions."
          },
          "content": {
            "type": "string"
          },
          "images": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/R2Image"
            },
            "description": "Images attached or generated for this message. As of v2.0.0 (replaces imageUrls: string[])."
          },
          "parentMessageId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "editedFromId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "modelUsed": {
            "type": [
              "string",
              "null"
            ],
            "description": "Model ID that produced this message, or null for user messages."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp.",
            "example": "2026-04-20T14:23:07.000Z"
          }
        },
        "required": [
          "id",
          "role",
          "content",
          "images",
          "parentMessageId",
          "editedFromId",
          "modelUsed",
          "createdAt"
        ]
      },
      "StarMessageResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "success"
        ]
      },
      "StarMessageBody": {
        "type": "object",
        "properties": {
          "starred": {
            "type": "boolean"
          }
        },
        "required": [
          "starred"
        ]
      },
      "DeleteMessageResponse": {
        "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"
        ]
      },
      "EditMessageResponse": {
        "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"
        ]
      },
      "EditMessageBody": {
        "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"
        ]
      },
      "RegenerateMessageBody": {
        "type": "object",
        "properties": {
          "model": {
            "type": "string",
            "description": "Override model for this regenerate. If omitted, uses the conversation's current model."
          }
        }
      },
      "UpscaleMessageResponse": {
        "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"
        ]
      },
      "UpscaleMessageBody": {
        "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"
        ]
      },
      "ListConversationsResponse": {
        "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"
        ]
      },
      "ConversationSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "title": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp.",
            "example": "2026-04-20T14:23:07.000Z"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp.",
            "example": "2026-04-20T14:23:07.000Z"
          },
          "activeLeafId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "projectId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Project this conversation belongs to, or null if unassigned.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "projectPath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full slash-separated project path, e.g. \"smashparty/textures\". Null if unassigned."
          },
          "messageCount": {
            "type": "integer",
            "minimum": 0,
            "description": "Total messages in the conversation (all branches)."
          },
          "preview": {
            "type": "string",
            "description": "First 100 characters of the latest user message."
          },
          "hasStarred": {
            "type": "boolean",
            "description": "True if any message in this conversation is starred."
          },
          "lastAssistantPreview": {
            "type": [
              "string",
              "null"
            ],
            "description": "First 100 characters of the most recent AI message text (for hover preview)."
          },
          "lastImage": {
            "allOf": [
              {
                "$ref": "#/components/schemas/R2Image"
              },
              {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Most recent AI-generated image anywhere in the conversation (any branch), for agent + UI preview. Null if no images yet."
              }
            ]
          },
          "inflightSince": {
            "type": [
              "number",
              "null"
            ],
            "description": "Epoch milliseconds when generation started, if a request is currently in flight on this conversation. Null when idle or when an in-flight request has aged past the 5-minute abandonment window."
          },
          "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"
            ]
          }
        },
        "required": [
          "id",
          "title",
          "createdAt",
          "updatedAt",
          "activeLeafId",
          "projectId",
          "projectPath",
          "messageCount",
          "preview",
          "hasStarred",
          "lastAssistantPreview",
          "lastImage",
          "inflightSince",
          "styleDirective",
          "model",
          "aspectRatio",
          "imageSize",
          "searchGrounding",
          "thinkingLevel"
        ]
      },
      "CreateConversationResponse": {
        "type": "object",
        "properties": {
          "conversation": {
            "$ref": "#/components/schemas/ConversationSummary"
          }
        },
        "required": [
          "conversation"
        ]
      },
      "CreateConversationBody": {
        "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"
          }
        }
      },
      "GetConversationResponse": {
        "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"
        ]
      },
      "MessageTreeNode": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "parentMessageId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "role": {
            "type": "string",
            "enum": [
              "user",
              "assistant"
            ]
          },
          "content": {
            "type": "string",
            "description": "Truncated to first 100 characters."
          },
          "isActive": {
            "type": "boolean",
            "description": "True if this message is on the currently-active branch."
          },
          "starred": {
            "type": "boolean",
            "description": "User-flagged star state (Plan 04)."
          },
          "imageCount": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of images attached (references or generated)."
          },
          "modelUsed": {
            "type": [
              "string",
              "null"
            ],
            "description": "Model ID that produced this message, or null for user messages."
          },
          "durationMs": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Generation latency from the latest debug log, if available."
          }
        },
        "required": [
          "id",
          "parentMessageId",
          "role",
          "content",
          "isActive",
          "starred",
          "imageCount",
          "modelUsed",
          "durationMs"
        ]
      },
      "RetryStatus": {
        "type": "object",
        "properties": {
          "models": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/RetryScopeStatus"
            },
            "description": "Per-model retry status, keyed by model ID."
          },
          "project": {
            "allOf": [
              {
                "$ref": "#/components/schemas/RetryScopeStatus"
              },
              {
                "description": "Project-wide retry status (for 429 rate limits)."
              }
            ]
          }
        },
        "required": [
          "models",
          "project"
        ]
      },
      "RetryScopeStatus": {
        "type": "object",
        "properties": {
          "retryable": {
            "type": "boolean"
          },
          "retryAfter": {
            "type": [
              "number",
              "null"
            ]
          },
          "failureCount": {
            "type": "integer",
            "minimum": 0
          },
          "lastErrorAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO 8601 timestamp.",
            "example": "2026-04-20T14:23:07.000Z"
          }
        },
        "required": [
          "retryable",
          "retryAfter",
          "failureCount",
          "lastErrorAt"
        ]
      },
      "UpdateConversationResponse": {
        "type": "object",
        "properties": {
          "conversation": {
            "$ref": "#/components/schemas/ConversationSummary"
          }
        },
        "required": [
          "conversation"
        ]
      },
      "UpdateConversationBody": {
        "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"
          }
        }
      },
      "SwitchBranchResponse": {
        "type": "object",
        "properties": {
          "activeLeafId": {
            "type": "string",
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          }
        },
        "required": [
          "activeLeafId"
        ]
      },
      "SwitchBranchBody": {
        "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"
        ]
      },
      "ListModelsResponse": {
        "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"
        ]
      },
      "ModelInfo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Model ID (e.g. 'gemini-3.1-flash-image-preview')."
          },
          "label": {
            "type": "string",
            "description": "Display label (e.g. 'Banana 2 🍌')."
          },
          "icon": {
            "type": "string"
          },
          "supportsThinking": {
            "type": "boolean"
          },
          "supportsSearchGrounding": {
            "type": "boolean"
          },
          "maxImageSize": {
            "type": "string",
            "description": "Largest supported imageSize (e.g. '4096x4096')."
          },
          "health": {
            "$ref": "#/components/schemas/RetryScopeStatus"
          }
        },
        "required": [
          "id",
          "label",
          "supportsThinking",
          "supportsSearchGrounding",
          "maxImageSize",
          "health"
        ]
      },
      "GetUpscaleResponse": {
        "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"
        ]
      },
      "ChangelogResponse": {
        "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"
        ]
      },
      "ChangelogEntry": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string",
            "description": "Semver (e.g. '1.1.0')."
          },
          "date": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp.",
            "example": "2026-04-20T14:23:07.000Z"
          },
          "summary": {
            "type": "string"
          },
          "breakingChanges": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "additions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "fixes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "version",
          "date",
          "summary",
          "breakingChanges",
          "additions",
          "fixes"
        ]
      },
      "ListProjectsResponse": {
        "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"
        ]
      },
      "ProjectNode": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "parentId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "name": {
            "type": "string"
          },
          "path": {
            "type": "string",
            "description": "Slash-separated path from root, e.g. \"smashparty/textures/grass\""
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp.",
            "example": "2026-04-20T14:23:07.000Z"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp.",
            "example": "2026-04-20T14:23:07.000Z"
          },
          "conversationCount": {
            "type": "integer",
            "minimum": 0
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectNode"
            }
          }
        },
        "required": [
          "id",
          "parentId",
          "name",
          "path",
          "createdAt",
          "updatedAt",
          "conversationCount",
          "children"
        ]
      },
      "ProjectRecentEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "name": {
            "type": "string",
            "description": "Leaf name only (e.g. 'character', not 'smashparty/character')."
          },
          "parentId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "fullPath": {
            "type": "string",
            "description": "Slash-separated breadcrumb for HoverCard tooltip."
          },
          "lastActivityAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp.",
            "example": "2026-04-20T14:23:07.000Z"
          },
          "lastActiveConversationId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "lastImage": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "url": {
                "type": "string"
              },
              "thumbUrl": {
                "type": "string"
              }
            },
            "required": [
              "url",
              "thumbUrl"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "parentId",
          "fullPath",
          "lastActivityAt",
          "lastActiveConversationId",
          "lastImage"
        ]
      },
      "CreateProjectResponse": {
        "type": "object",
        "properties": {
          "project": {
            "$ref": "#/components/schemas/ProjectNode"
          }
        },
        "required": [
          "project"
        ]
      },
      "CreateProjectBody": {
        "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"
        ]
      },
      "GetProjectResponse": {
        "type": "object",
        "properties": {
          "project": {
            "$ref": "#/components/schemas/ProjectNode"
          }
        },
        "required": [
          "project"
        ]
      },
      "UpdateProjectResponse": {
        "type": "object",
        "properties": {
          "project": {
            "$ref": "#/components/schemas/ProjectNode"
          }
        },
        "required": [
          "project"
        ]
      },
      "UpdateProjectBody": {
        "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"
          }
        }
      },
      "DeleteProjectResponse": {
        "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"
        ]
      },
      "GetSettingsResponse": {
        "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"
        ]
      },
      "UpdateSettingsBody": {
        "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"
            ]
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string"
          },
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "hits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchHit"
            }
          }
        },
        "required": [
          "query",
          "total",
          "hits"
        ]
      },
      "SearchHit": {
        "type": "object",
        "properties": {
          "conversationId": {
            "type": "string",
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "conversationTitle": {
            "type": "string"
          },
          "messageId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "UUIDv4 identifier.",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "snippet": {
            "type": "string",
            "description": "Excerpt with <mark> tags around the match, 20 chars of context on each side."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp.",
            "example": "2026-04-20T14:23:07.000Z"
          },
          "images": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/R2Image"
            },
            "description": "Images attached to the matching message, if any. Empty array for text-only messages or title matches. As of v2.0.0 — gives agents a cheap way to scan visual results without fetching the full conversation."
          }
        },
        "required": [
          "conversationId",
          "conversationTitle",
          "messageId",
          "snippet",
          "createdAt",
          "images"
        ]
      },
      "AdminCleanupResponse": {
        "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"
        ]
      },
      "AdminCleanupBody": {
        "type": "object",
        "properties": {
          "dryRun": {
            "type": "boolean",
            "default": true
          }
        }
      }
    },
    "parameters": {}
  },
  "paths": {
    "/api/chat": {
      "post": {
        "operationId": "sendChatMessage",
        "summary": "Send a prompt and receive an AI-generated image",
        "description": "Primary image generation endpoint. Accepts optional reference images inlined as base64 data URLs. The response includes R2 URLs for generated images.",
        "tags": [
          "Generation"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "If provided, repeated requests with the same key return the cached response (24h TTL)."
            },
            "required": false,
            "description": "If provided, repeated requests with the same key return the cached response (24h TTL).",
            "name": "Idempotency-Key",
            "in": "header"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendChatBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendChatResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/images": {
      "post": {
        "operationId": "generateImage",
        "summary": "Generate an image (alias for POST /api/chat)",
        "description": "Identical to POST /api/chat — intuitive URL for image generation workflows.",
        "tags": [
          "Generation"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "If provided, repeated requests with the same key return the cached response (24h TTL)."
            },
            "required": false,
            "description": "If provided, repeated requests with the same key return the cached response (24h TTL).",
            "name": "Idempotency-Key",
            "in": "header"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendChatBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendChatResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/messages/{id}": {
      "get": {
        "operationId": "getMessage",
        "summary": "Get a single message with sibling info and upscales",
        "tags": [
          "Messages"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMessageResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "starMessage",
        "summary": "Toggle the starred flag on a message",
        "description": "Simple body {starred: boolean}. For renaming/branching see /edit.",
        "tags": [
          "Messages"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StarMessageBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StarMessageResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteMessage",
        "summary": "Delete a message and all its descendants (cascade + R2 cleanup)",
        "description": "Recursively deletes the message subtree, removes R2 objects, and moves activeLeafId to the parent if it was inside the deleted subtree.",
        "tags": [
          "Messages"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteMessageResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/messages/{id}/edit": {
      "post": {
        "operationId": "editMessage",
        "summary": "Edit a user message (creates a branch)",
        "tags": [
          "Messages"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "If provided, repeated requests with the same key return the cached response (24h TTL)."
            },
            "required": false,
            "description": "If provided, repeated requests with the same key return the cached response (24h TTL).",
            "name": "Idempotency-Key",
            "in": "header"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EditMessageBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EditMessageResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/messages/{id}/regenerate": {
      "post": {
        "operationId": "regenerateMessage",
        "summary": "Regenerate an AI response (creates a sibling)",
        "tags": [
          "Messages"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "If provided, repeated requests with the same key return the cached response (24h TTL)."
            },
            "required": false,
            "description": "If provided, repeated requests with the same key return the cached response (24h TTL).",
            "name": "Idempotency-Key",
            "in": "header"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegenerateMessageBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EditMessageResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/messages/{id}/upscale": {
      "post": {
        "operationId": "upscaleMessage",
        "summary": "Upscale an AI-generated image (2x or 4x)",
        "tags": [
          "Messages"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpscaleMessageBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpscaleMessageResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/conversations": {
      "get": {
        "operationId": "listConversations",
        "summary": "List all conversations",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "Search query (FTS5 across messages + titles)."
            },
            "required": false,
            "description": "Search query (FTS5 across messages + titles).",
            "name": "q",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Filter by project ID. Use \"none\" for unassigned conversations."
            },
            "required": false,
            "description": "Filter by project ID. Use \"none\" for unassigned conversations.",
            "name": "projectId",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "If \"true\", returns only conversations that contain at least one starred message. Intersects with projectId + q."
            },
            "required": false,
            "description": "If \"true\", returns only conversations that contain at least one starred message. Intersects with projectId + q.",
            "name": "starred",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            },
            "required": false,
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0
            },
            "required": false,
            "name": "offset",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListConversationsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createConversation",
        "summary": "Create a new conversation",
        "tags": [
          "Conversations"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateConversationBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateConversationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/conversations/{id}": {
      "get": {
        "operationId": "getConversation",
        "summary": "Get conversation with messages, tree, retry status",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetConversationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateConversation",
        "summary": "Update conversation settings (title, model, aspectRatio, etc.)",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateConversationBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdateConversationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteConversation",
        "summary": "Delete a conversation and all its messages",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "success"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/conversations/{id}/switch-branch": {
      "post": {
        "operationId": "switchBranch",
        "summary": "Switch to a different branch in the conversation tree",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SwitchBranchBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SwitchBranchResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/models": {
      "get": {
        "operationId": "listModels",
        "summary": "List available models with health + retry status",
        "tags": [
          "Models"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListModelsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/upscales/{id}": {
      "get": {
        "operationId": "getUpscale",
        "summary": "Get an upscaled image record",
        "tags": [
          "Upscales"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetUpscaleResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/changelog": {
      "get": {
        "operationId": "getChangelog",
        "summary": "Machine-readable changelog for API versions",
        "tags": [
          "Meta"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChangelogResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/projects": {
      "get": {
        "operationId": "listProjects",
        "summary": "List all projects as a nested tree",
        "description": "Returns the full project tree with computed paths and per-node conversation counts. Infinite nesting supported.",
        "tags": [
          "Projects"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListProjectsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createProject",
        "summary": "Create a project (root or child)",
        "description": "Create a new project. Omit parentId (or pass null) for a root project.",
        "tags": [
          "Projects"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateProjectBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateProjectResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate project name among siblings",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/projects/{id}": {
      "get": {
        "operationId": "getProject",
        "summary": "Get a single project with its subtree",
        "tags": [
          "Projects"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetProjectResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateProject",
        "summary": "Rename or move a project",
        "description": "Cycle-protected: refuses to move a project under its own descendant. Duplicate sibling names are rejected with 409.",
        "tags": [
          "Projects"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateProjectBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdateProjectResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Duplicate project name among siblings",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteProject",
        "summary": "Delete a project (cascades to descendants)",
        "description": "Deletes the project and all descendants. Conversations assigned to any deleted project get project_id = NULL (they survive).",
        "tags": [
          "Projects"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteProjectResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings": {
      "get": {
        "operationId": "getSettings",
        "summary": "Get global default settings",
        "description": "Returns the single global settings row. New conversations inherit these values. Per-user settings arrive with auth.",
        "tags": [
          "Settings"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetSettingsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateSettings",
        "summary": "Update global default settings",
        "description": "Update any subset of fields. Only provided fields are changed. Pass null to clear a field.",
        "tags": [
          "Settings"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSettingsBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetSettingsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/search": {
      "get": {
        "operationId": "search",
        "summary": "Full-text search across messages and conversation titles",
        "description": "Uses the messages_fts FTS5 virtual table for message hits and a LIKE scan for title hits. Snippets include <mark> tags. Results deduped by conversation.",
        "tags": [
          "Search"
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "Search query. FTS5 phrase match."
            },
            "required": true,
            "description": "Search query. FTS5 phrase match.",
            "name": "q",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "required": false,
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0
            },
            "required": false,
            "name": "offset",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/admin/cleanup": {
      "post": {
        "operationId": "adminCleanup",
        "summary": "Purge imageless error-noise conversations",
        "description": "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.",
        "tags": [
          "Admin"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdminCleanupBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success (preview or executed)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminCleanupResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Conflict (e.g. concurrent admin cleanup in progress)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported content type",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {}
}
